mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
fix(security): sanitize provider HTTP errors
Preserve provider error status without allowing HTTP-shaped messages or details to bypass repository credential redaction. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
4613b36f58
commit
b2668d5713
2 files changed
+33
-5
No files matched your search
@@ -518,6 +518,34 @@ describe("bridge connector", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sanitizes HTTP-shaped provider errors while preserving their status", async () => {
|
||||
const connector = createPluginRepositoryProviderConnector({
|
||||
pluginId: PLUGIN_ID,
|
||||
pluginKey: PLUGIN_KEY,
|
||||
declaration,
|
||||
workerManager: fakeWorkerManager({
|
||||
handlers: {
|
||||
repositoryProviderSync: () => {
|
||||
const error = new Error("provider failed token=ghs_supersecret at https://user:pw@github.example.com/x") as Error & {
|
||||
status: number;
|
||||
details: unknown;
|
||||
};
|
||||
error.status = 409;
|
||||
error.details = { access_token: "ghs_supersecret" };
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await connector.sync({ connection: connection(), cursor: null }).catch((error: Error & { status?: number; details?: unknown }) => {
|
||||
expect(error.status).toBe(409);
|
||||
expect(error.message).not.toContain("ghs_supersecret");
|
||||
expect(error.message).not.toContain("user:pw@");
|
||||
expect(error.details).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("rebuilds the credential audit from host-known values and marks it no-store", async () => {
|
||||
const conn = connection();
|
||||
const connector = createPluginRepositoryProviderConnector({
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
REPOSITORY_PROVIDER_ANY_HOST,
|
||||
normalizeRepositoryProviderHost,
|
||||
} from "@paperclipai/shared";
|
||||
import { unprocessable } from "../../errors.js";
|
||||
import { HttpError, unprocessable } from "../../errors.js";
|
||||
import {
|
||||
sanitizeRepositoryProviderError,
|
||||
sanitizeRepositoryProviderMetadata,
|
||||
@@ -160,11 +160,11 @@ async function guarded<T>(operation: () => Promise<T>): Promise<T> {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
// Providers surface HTTP-shaped errors deliberately (422 for bad state,
|
||||
// 409 for conflicts); those pass through with their status intact. Anything
|
||||
// else becomes a sanitized 422 so a raw provider message — which may embed a
|
||||
// token or an internal URL — never reaches a caller or a log line.
|
||||
// 409 for conflicts). Preserve only their status; their message and details
|
||||
// are still untrusted provider output and must not bypass sanitization.
|
||||
if (error && typeof error === "object" && typeof (error as { status?: unknown }).status === "number") {
|
||||
throw error;
|
||||
const status = (error as { status: number }).status;
|
||||
throw new HttpError(status, sanitizeRepositoryProviderError(error));
|
||||
}
|
||||
throw unprocessable(sanitizeRepositoryProviderError(error));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user