packages/console/src/features/sandboxes/envd-client.ts:227-248:
export async function signedDownloadUrl(
auth: EnvdAuth,
path: string,
): Promise<string> {
const exp = Math.floor(Date.now() / 1000) + SIGNED_URL_TTL_SECONDS;
const material = `${path}:read::${auth.envdAccessToken}:${exp}`;
const digest = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(material),
);
sha256(secret || data) is the textbook construction HMAC exists to replace. Merkle–Damgård hashes are length-extendable: given H(m) and len(m), an attacker can compute H(m || pad || suffix) without knowing m.
Is it exploitable today? Probably not, and I want to be precise rather than alarmist about why: the secret is at position four of five, the trailing :${exp} is attacker-visible, and the verifier (server/e2b/signing.ts, per the comment at line 213) reconstructs the string from parsed query params rather than accepting an arbitrary tail — so there is no obvious place to graft an extension. The concern is that the reason it is safe is an accident of field ordering in a format that is duplicated across two codebases and pinned by an e2e test. Reorder the fields, add a sixth optional component, or let any component be attacker-controlled and open-ended, and the construction becomes forgeable — with no test that would notice, because a forged signature is by definition one the verifier accepts.
Two more properties of the current scheme worth weighing:
- No revocation. Once minted, the URL is valid for its full 15 minutes (
SIGNED_URL_TTL_SECONDS, line 210) against the daemon's public origin (line 248: ${window.location.origin}/files?...), for anyone who has the string, with the wildcard-CORS door in server/e2b/cors.ts:17 open to any origin. Destroying the sandbox or rotating the API token does not invalidate it — only the clock does. That is a deliberate design (the comment at 206-209 argues the TTL trade-off well), but it should be stated as "unrevocable capability", not just "过期是一次点击的事".
btoa(String.fromCharCode(...new Uint8Array(digest))) (line 237) spreads a 32-byte array, which is fine at this size but is the pattern that blows the stack when someone reuses the helper on a larger buffer.
Recommendation: switch both sides to crypto.subtle.importKey('raw', token, {name:'HMAC', hash:'SHA-256'}) + crypto.subtle.sign. It is the same number of lines, removes the ordering dependency entirely, and the server side (node:crypto's createHmac) is already using HMAC for the session cookie in auth.ts:127 — so the codebase is inconsistent with itself here.
packages/console/src/features/sandboxes/envd-client.ts:227-248:sha256(secret || data)is the textbook construction HMAC exists to replace. Merkle–Damgård hashes are length-extendable: givenH(m)andlen(m), an attacker can computeH(m || pad || suffix)without knowingm.Is it exploitable today? Probably not, and I want to be precise rather than alarmist about why: the secret is at position four of five, the trailing
:${exp}is attacker-visible, and the verifier (server/e2b/signing.ts, per the comment at line 213) reconstructs the string from parsed query params rather than accepting an arbitrary tail — so there is no obvious place to graft an extension. The concern is that the reason it is safe is an accident of field ordering in a format that is duplicated across two codebases and pinned by an e2e test. Reorder the fields, add a sixth optional component, or let any component be attacker-controlled and open-ended, and the construction becomes forgeable — with no test that would notice, because a forged signature is by definition one the verifier accepts.Two more properties of the current scheme worth weighing:
SIGNED_URL_TTL_SECONDS, line 210) against the daemon's public origin (line 248:${window.location.origin}/files?...), for anyone who has the string, with the wildcard-CORS door inserver/e2b/cors.ts:17open to any origin. Destroying the sandbox or rotating the API token does not invalidate it — only the clock does. That is a deliberate design (the comment at 206-209 argues the TTL trade-off well), but it should be stated as "unrevocable capability", not just "过期是一次点击的事".btoa(String.fromCharCode(...new Uint8Array(digest)))(line 237) spreads a 32-byte array, which is fine at this size but is the pattern that blows the stack when someone reuses the helper on a larger buffer.Recommendation: switch both sides to
crypto.subtle.importKey('raw', token, {name:'HMAC', hash:'SHA-256'})+crypto.subtle.sign. It is the same number of lines, removes the ordering dependency entirely, and the server side (node:crypto'screateHmac) is already using HMAC for the session cookie inauth.ts:127— so the codebase is inconsistent with itself here.