fix(contract): reject control characters in the scan target remote - #233
Open
rohanpoudel2 wants to merge 2 commits into
Open
fix(contract): reject control characters in the scan target remote#233rohanpoudel2 wants to merge 2 commits into
rohanpoudel2 wants to merge 2 commits into
Conversation
`validateCanonicalContract` extracted the URL authority with a regex whose negated class `[^/?#]` matches ASCII tab, LF and CR, and the follow-up `new URL(remote)` check did not catch them either: the WHATWG URL parser strips exactly those three characters from its input before parsing. The value that survived validation was therefore not the value that was validated, and the unsanitized original was handed back to callers as `manifest.scan.target.remote`. A manifest carrying "https://example.com\nhttps://evil.example.net" loaded successfully while `new URL()` saw host "example.comhttps". Reject C0/C1 control characters and Unicode line separators in `remote` before parsing it, mirroring the class `requireModelSafeOutputDir` already applies in runtime.ts for the same reason. Nothing under src/ consumes `remote` today, so this closes a broken guarantee rather than a live exploit: SDK consumers that render or re-emit the field were inheriting a multi-line value that had passed validation. Fixes openai#231
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #231
Problem
validateCanonicalContractvalidatedscan.target.remoteagainst a string that was not the string it stored.The authority is extracted with:
[^/?#]matches ASCII tab, LF and CR. The follow-upnew URL(remote)check does not catch them either, because the WHATWG URL parser strips exactly those three characters from its input before parsing. So a manifest carrying:loaded successfully —
new URL()saw hostexample.comhttps, whileloadContracthanded the original two-line string straight back to callers.Neither surrounding guard closes it: the schema pattern
^(?![^:/?#]+://[^/?#]*@)[^?#]+$also matches newlines, andvalidateParsedJsonchecks only for well-formed Unicode.Change
Reject C0/C1 control characters and Unicode line separators in
remotebefore the regex and URL checks run, mirroring the classrequireModelSafeOutputDiralready applies inruntime.tsfor the same reason. That constant is not exported, so it is mirrored locally with a comment naming the source of truth rather than wideningruntime.ts's public surface for one caller.Why not also require
href === remoteI tested this and rejected it. The WHATWG serializer legitimately rewrites valid URLs — it strips a default
:443, lowercases mixed-case hostnames, and punycode-encodes internationalized ones — so exact canonical equality would reject real remotes. The bundled producer (finalize_scan_contract.py) validatesremoteonly viaurlsplitscheme and netloc checks and imposes no canonical-equality requirement, so it can legitimately emit any of those forms.That trade would only be worth it if the URL parser smuggled a wider set of characters. It does not. Enumerating every C0/C1 code point through
new URL("https://exa<c>mple.com"):Only tab, LF and CR slip through; the other 62 already make
new URL()throw. The narrow control-character rejection is therefore sufficient, and the broader C0/C1 class is defence in depth consistent withruntime.ts.Impact, stated plainly
This closes a broken guarantee rather than a live exploit. Nothing under
src/consumes.remotetoday, and credential smuggling remains correctly blocked — any@in the authority is still rejected before URL parsing, and that check is untouched. The value is that the contract layer is precisely where an untrusted manifest field is meant to be sanitized, and SDK consumers that render, log or re-emitmanifest.scan.target.remotewere inheriting a multi-line value that had passed validation.Verification
Three rejection cases (
\n,\r,\t) were added to the existing table-driven "rejects schema-valid but canonically invalid contract data" test, plus a standalone test proving an ordinaryhttps://github.com/example/reporemote still loads and round-trips unchanged — so the fix cannot pass by rejecting everything.Against the unfixed source the new case fails with
Expected promise that rejects / Received promise that resolved; with the fix,contract.test.tsis30 pass / 0 fail.Full suite: 718 pass / 5 skip / 0 fail (717 baseline plus the new test).
pnpm run typesandpnpm run formatare clean.