Skip to content

The signed-URL format is the SDK's: write it down and pin the field order - #105

Open
ursasi wants to merge 1 commit into
BitMiracle-AI:mainfrom
ursasi:signed-url-format-is-inherited
Open

ursasi wants to merge 1 commit into
BitMiracle-AI:mainfrom
ursasi:signed-url-format-is-inherited

Conversation

@ursasi

@ursasi ursasi commented Oct 7, 2026

Copy link
Copy Markdown

Related to #90. I did not switch to HMAC, and I think that part of the report
would do harm: the construction is not ours to pick.

What the format is

It is the E2B wire format, on both ends. The official SDK hashes exactly this
string before a URL ever reaches us — packages/js-sdk/src/sandbox/signature.ts
builds path:operation:user:envdAccessToken[:exp] and sends it through
crypto.subtle.digest('SHA-256', …) — and real envd verifies the same digest.
This repo already says so in three places (signing.ts's header,
envd/files.ts:186, and app.test.ts's "the SDK's formula, rewritten rather
than imported"), and the daemon has a test that accepts an SDK-minted
signature. An HMAC on the verification side would reject every URL minted by
the SDK the project exists to be compatible with.

On the length-extension worry: the construction has no graft point, and not by
luck of one field's position — the verifier rebuilds the material from parsed
query params in a fixed order, the token is fourth of five, and the only
component that can follow it parses as a number. There is no place for an
attacker-chosen suffix to survive that reading. What the report rightly flags
is that this reasoning should be written down where the next reader will find
it instead of being re-derived — that is what this PR does.

On "no test would notice" a reordering: several already would (app.test.ts
rewrites the SDK's formula, compat.test.ts and sandbox-proxy.test.ts mint
it, e2e/src/console.test.ts mints it in the browser). I added the missing
one: a signature over a reordered material, and one over a material with an
extra component, both refused.

What changed

  • signing.ts and envd-client.ts state that the shape is inherited, name the
    SDK file, and spell out why there is no extension point. Same for the TTL:
    the comment now says the URL is a 15-minute capability with no revocation —
    destroying the sandbox or rotating the API token does not end it.
  • btoa(String.fromCharCode(...bytes)) became a loop. This part of the report
    is real: the spread throws RangeError once the buffer grows (I measured
    400 KB). The conversion output is byte-for-byte identical, checked against
    the spread on the same digest.
  • New compat test: reordered and extended materials answer 401, canonical order
    still opens the door.

Verification

pnpm vitest run in packages/server: 651 passed, 2 skipped (the Docker
contract suite). tsc --noEmit clean; the console file type-checks too
(target ES2023, so the loop is fine). biome check reports the same 10
pre-existing noUnsafeOptionalChaining findings in compat.test.ts as before
the change.

If you do want a signature scheme of our own on top of the SDK-compatible one,
I am happy to work on that as a separate design — but it has to be a second
URL form, not a change to this one.

…d a test pins the field order

BitMiracle-AI#90 reads sha256(path:operation:user:token[:exp]) as sha256(secret || data) and
asks for HMAC on both ends. That would break the one thing the format exists
for: the official SDK hashes exactly this string before a URL ever reaches us
(js-sdk/src/sandbox/signature.ts sends it through crypto.subtle SHA-256), and
real envd verifies the same digest — an HMAC on this side rejects every
SDK-minted URL.

So the change is the part of the report that is ours to act on:

- envd-client.ts and signing.ts now say the shape is inherited, name the SDK
  file, and spell out why length extension has no graft point in it: the
  material is rebuilt from parsed params in this order, the token sits fourth
  of five, and the only possible trailing component parses as a number.
- The TTL comment states what the URL is: a 15-minute capability with no
  revocation — destroying the sandbox or rotating the API token does not end
  it, only the clock does.
- btoa(String.fromCharCode(...bytes)) becomes a loop. The spread throws
  RangeError once the buffer grows (measured at 400 KB) and the conversion is
  byte-for-byte the same, so the console keeps minting what the SDK mints.
- A compat test pins the field order as a contract: a reordered material and
  one carrying an extra component both answer 401, and the canonical order
  still opens the door.
@ursasi
ursasi force-pushed the signed-url-format-is-inherited branch from c93df2f to 2c1b9e2 Compare October 7, 2026 14:06
@ursasi ursasi changed the title The signed-URL format is the SDK's, not ours: a test pins the field order, and the comments say why The signed-URL format is the SDK's: write it down and pin the field order Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant