fix(video/vertex): reject job ids and model ids that escape the URL path
A Vertex job id is a base64url-encoded operation name, and base64url decoding
accepts arbitrary bytes without throwing, so the previous decode-and-split
check let a crafted id splice a path traversal into the fetch URL while the
Bearer token stayed attached — e.g. "..%2F..%2Fevil" resolved to
/v1/evil:fetchPredictOperation on the Vertex host. body.model had the same
shape on the create path, where it is interpolated into the URL unescaped.
decodeJobId now requires a charset-only id, a byte-for-byte round-trip, and a
decoded name matching ^projects/{p}/locations/{l}/publishers/{pub}/models/{m}/operations/{op}$
— no field may contain "/", so ".." can never reach the URL. model ids are
restricted to [A-Za-z0-9._-].
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -260,4 +260,37 @@ describe("vertex (veo) video adapter", () => {
|
||||
expect(result.status).toBe(400);
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// A base64url id decodes to arbitrary bytes, so a crafted one used to splice a
|
||||
// path traversal into the fetch URL while the Authorization header stayed on.
|
||||
it("rejects job ids that decode outside the projects/…/operations/ shape", async () => {
|
||||
refreshVertexToken.mockResolvedValue({ accessToken: "vertex-tok" });
|
||||
const jid = (s) => Buffer.from(s, "utf8").toString("base64url");
|
||||
|
||||
for (const id of [
|
||||
jid("../../evil"),
|
||||
jid("projects/p/locations/l/publishers/google/models/m/operations/../../x"),
|
||||
jid("../../evil/operations/op"),
|
||||
"!!!not-base64!!!",
|
||||
`${JOB_ID}=`,
|
||||
`${JOB_ID}\n`,
|
||||
]) {
|
||||
const result = await handleVideoProxyCore({ provider: "vertex", requestId: id, credentials: { apiKey: saJson } });
|
||||
expect(result.status).toBe(400);
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a model id carrying path separators", async () => {
|
||||
refreshVertexToken.mockResolvedValue({ accessToken: "vertex-tok" });
|
||||
const result = await handleVideoProxyCore({
|
||||
provider: "vertex",
|
||||
action: "generations",
|
||||
rawBody: JSON.stringify({ model: "../../evil", prompt: "x" }),
|
||||
contentType: "application/json",
|
||||
credentials: { apiKey: saJson },
|
||||
});
|
||||
expect(result.status).toBe(400);
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user