Skip to content

[WRONG BRANCH] web-search: validate and sanitize citation sources before relaying - #183

Draft
luvs01 wants to merge 1 commit into
mainfrom
codex/fix-unvalidated-web-citation-urls
Draft

[WRONG BRANCH] web-search: validate and sanitize citation sources before relaying#183
luvs01 wants to merge 1 commit into
mainfrom
codex/fix-unvalidated-web-citation-urls

Conversation

@luvs01

@luvs01 luvs01 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Prevent untrusted sidecar-provided citation data from reaching clients as url_citation annotations and introducing unsafe URL schemes, control characters, oversized titles, or excessive/oversized source lists.

Description

  • Add a centralized sanitizer in src/web-search/sources.ts that enforces HTTP(S) scheme, rejects control characters, bounds URL/title byte lengths, caps per-message count and aggregate bytes, and deduplicates by URL via appendSafeWebSearchSource/safeWebSearchSources.
  • Apply validation when parsing sidecar output in src/web-search/parse.ts so structured url_citation annotations and trailing Markdown Sources: lists are filtered before being collected as sources.
  • Sanitize again in the Responses bridge (src/bridge.ts) for both streaming and non-streaming paths by converting incoming web_search_call_end sources through the safe filter and using the append helper when queuing pendingWebSources for the next assistant message.
  • Add a regression test in tests/bridge.test.ts that asserts unsafe schemes, control characters, oversized titles, and excess entries are not relayed to streaming or non-streaming clients.

Testing

  • Ran bun test tests/bridge.test.ts, which passed including the new regression for filtered citations.
  • Ran bun run typecheck (bun x tsc --noEmit), which succeeded with no type errors.
  • Ran bun run privacy:scan, which completed successfully in this environment.
  • Performed a fuller bun run test run; focused tests and typecheck are green, and an unrelated environment-sensitive permission test surfaced in the full suite (an EACCES expectation that depends on non-root runner conditions) that is not caused by these changes.

Codex Task

Summary by CodeRabbit

  • Bug Fixes

    • Improved web-search citation handling by filtering invalid, unsafe, duplicate, and oversized sources.
    • Applied consistent source limits and validation across streamed and non-streamed responses.
    • Prevented rejected citations from counting toward usage limits.
  • Tests

    • Added coverage confirming valid citations are preserved while unsafe or excessive sources are excluded.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

⚠️ Wrong target branch

This pull request currently targets main, but pull requests must target one of dev.

@luvs01 Please retarget this PR to dev. All contributions go to dev; main receives only release promotions. See our Contributing guide for details. Thanks! 🙏

Its title has been prefixed with [WRONG BRANCH].

This pull request is being kept as a draft automatically. Once every issue above is resolved, it will be marked ready for review again.

@github-actions github-actions Bot changed the title web-search: validate and sanitize citation sources before relaying [WRONG BRANCH] web-search: validate and sanitize citation sources before relaying Aug 9, 2026
@github-actions
github-actions Bot marked this pull request as draft August 9, 2026 01:25
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Web-search source handling now validates URLs and titles, rejects unsafe or oversized entries, deduplicates sources, enforces count and payload limits, and applies the same rules to parsed, streaming, and batch Responses output.

Changes

Web-search source handling

Layer / File(s) Summary
Safe source validation contract
src/web-search/sources.ts
Defines SafeWebSearchSource, source count and byte limits, control-character checks, HTTP(S) URL validation, title validation, duplicate filtering, and aggregate payload enforcement.
Parser source collection
src/web-search/parse.ts
Annotation citations, trailing markdown sources, and streamed-source merges use appendSafeWebSearchSource for acceptance and deduplication.
Bridge output and budget handling
src/bridge.ts, tests/bridge.test.ts
Streaming and batch web-search completions sanitize output sources and pending citations. Only accepted sources consume budget. Tests cover unsafe URLs, control characters, oversized titles, citation limits, and output consistency.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebSearchParser
  participant SourceSanitizer
  participant Bridge
  participant ResponsesOutput
  participant TranslatorBudget

  WebSearchParser->>SourceSanitizer: append parsed and streamed sources
  Bridge->>SourceSanitizer: sanitize streaming or batch sources
  SourceSanitizer-->>Bridge: return accepted, deduplicated sources
  Bridge->>ResponsesOutput: emit accepted sources and citations
  Bridge->>TranslatorBudget: charge accepted sources only
Loading

Suggested reviewers: lidge-jun, wibias

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: validating and sanitizing web-search citation sources before relaying them.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-unvalidated-web-citation-urls

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bridge.ts`:
- Around line 1675-1680: Wrap the web_search_call_end switch case body in braces
so the safeSources declaration is scoped within its case and satisfies
noSwitchDeclarations; preserve the existing pushOutput behavior.
- Around line 1681-1685: Release the retained tool_search_sources budget when
pending web sources are transferred into annotations and the completed message
is retained by pushOutput. Update the relevant flushText path, matching the
release behavior already used by takeWebAnnotations, while preserving the
existing charge in appendSafeWebSearchSource.

In `@src/web-search/sources.ts`:
- Around line 28-30: Update the URL validation logic around parsed in the
citation URL function to reject candidates when parsed.username or
parsed.password is non-empty, before preserving or serializing the URL. Continue
accepting only http: and https: URLs without credentials.

In `@tests/bridge.test.ts`:
- Around line 1001-1013: Extend the test around bridgeToResponsesSSE and
buildResponseJSON to locate the streaming and batch web_search_call output items
and assert their sources equal the sanitized source list, including the expected
safe URL fields. Ensure both paths are validated so forwarding raw event.sources
cannot satisfy the existing assistant-message annotation assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2f2e8a79-fcdb-4a90-aed8-f5b5ca850b15

📥 Commits

Reviewing files that changed from the base of the PR and between 8a9c0ef and 7761481.

📒 Files selected for processing (4)
  • src/bridge.ts
  • src/web-search/parse.ts
  • src/web-search/sources.ts
  • tests/bridge.test.ts

Comment thread src/bridge.ts
Comment on lines +1675 to 1680
const safeSources = safeWebSearchSources(e.sources ?? []);
pushOutput({
type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
action: webSearchAction(e.queries),
...(e.sources && e.sources.length > 0 ? { sources: e.sources } : {}),
...(safeSources.length > 0 ? { sources: safeSources } : {}),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Wrap this switch case in a block.

Line 1675 declares safeSources directly in a switch case. Biome reports lint/correctness/noSwitchDeclarations for this declaration.

Wrap the web_search_call_end case body in braces so the lint check passes.

🧰 Tools
🪛 Biome (2.5.6)

[error] 1675-1675: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bridge.ts` around lines 1675 - 1680, Wrap the web_search_call_end switch
case body in braces so the safeSources declaration is scoped within its case and
satisfies noSwitchDeclarations; preserve the existing pushOutput behavior.

Source: Linters/SAST tools

Comment thread src/bridge.ts
Comment on lines +1681 to 1685
if (safeSources.length > 0) {
for (const s of safeSources) {
if (appendSafeWebSearchSource(pendingWebSources, s)) {
budget?.chargeRetained(bytesOf(JSON.stringify(s)), { kind: "tool_search_sources" });
pendingWebSources.push(s);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Release batch pending-source budget after annotation transfer.

Line 1684 charges each accepted source as tool_search_sources. flushText later clears pendingWebSources after it copies the sources into annotations, but it does not release this retained charge.

Repeated web-search and message cycles can exhaust the translator budget even when the final batch response fits. Release the pending-source bytes when pushOutput retains the completed message item, as the streaming path does in takeWebAnnotations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bridge.ts` around lines 1681 - 1685, Release the retained
tool_search_sources budget when pending web sources are transferred into
annotations and the completed message is retained by pushOutput. Update the
relevant flushText path, matching the release behavior already used by
takeWebAnnotations, while preserving the existing charge in
appendSafeWebSearchSource.

Comment thread src/web-search/sources.ts
Comment on lines +28 to +30
let parsed: URL;
try { parsed = new URL(candidate.url); } catch { return false; }
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject credentials in citation URLs.

Lines 28-30 accept https://token:secret@example.test/. The function then preserves that URL and serializes the credentials to clients.

Reject URLs when parsed.username or parsed.password is non-empty.

Proposed fix
-  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
+  if (
+    (parsed.protocol !== "http:" && parsed.protocol !== "https:")
+    || parsed.username
+    || parsed.password
+  ) return false;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let parsed: URL;
try { parsed = new URL(candidate.url); } catch { return false; }
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
let parsed: URL;
try { parsed = new URL(candidate.url); } catch { return false; }
if (
(parsed.protocol !== "http:" && parsed.protocol !== "https:")
|| parsed.username
|| parsed.password
) return false;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/web-search/sources.ts` around lines 28 - 30, Update the URL validation
logic around parsed in the citation URL function to reject candidates when
parsed.username or parsed.password is non-empty, before preserving or
serializing the URL. Continue accepting only http: and https: URLs without
credentials.

Source: Path instructions

Comment thread tests/bridge.test.ts
Comment on lines +1001 to +1013
const frames = await collectSse(bridgeToResponsesSSE(replay(events), "routed/model"));
const done = frames.find(f => f.event === "response.output_item.done"
&& (f.data.item as Record<string, unknown>)?.type === "message");
const streamingPart = ((done!.data.item as Record<string, unknown>).content as Record<string, unknown>[])[0];
expect((streamingPart.annotations as unknown[]).slice(0, 1)).toEqual([
{ type: "url_citation", url: "https://safe.test/docs", title: "Safe docs", start_index: 0, end_index: 0 },
]);
expect(streamingPart.annotations).toHaveLength(20);

const json = buildResponseJSON(events, "routed/model");
const message = (json.output as Record<string, unknown>[]).find(item => item.type === "message")!;
const batchPart = (message.content as Record<string, unknown>[])[0];
expect(batchPart.annotations).toEqual(streamingPart.annotations);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Assert sanitized sources on web_search_call output items.

The test only checks assistant-message annotations. web_search_call.sources is also sent to clients.

If closeCurrentWebSearch or the batch output path later forwards raw event.sources, these assertions can still pass. Assert the sanitized source list on the streaming and batch web_search_call items too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/bridge.test.ts` around lines 1001 - 1013, Extend the test around
bridgeToResponsesSSE and buildResponseJSON to locate the streaming and batch
web_search_call output items and assert their sources equal the sanitized source
list, including the expected safe URL fields. Ensure both paths are validated so
forwarding raw event.sources cannot satisfy the existing assistant-message
annotation assertions.

Source: Path instructions

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 776148183d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/web-search/sources.ts
Comment on lines +42 to +43
const usedBytes = target.reduce((sum, item) => sum + byteLength(item.url) + byteLength(item.title ?? ""), 0);
if (usedBytes + byteLength(next.url) + byteLength(next.title ?? "") > MAX_WEB_SEARCH_SOURCE_BYTES) return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Measure the serialized citation budget

When an untrusted URL or title contains JSON-escaped characters such as quotes or backslashes, this sums the raw UTF-8 strings even though both bridge paths relay their JSON serialization. For example, eight accepted HTTPS URLs containing repeated quotes total 16,128 bytes here but serialize to 32,217 bytes, bypassing the intended 16 KiB aggregate cap. Compute the budget from the serialized representation, including citation wrapper overhead, or retain and measure a normalized URL.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant