Skip to content

fix(security): validate URL scheme before rendering href to block javascript: XSS#102

Open
aaronjmars wants to merge 2 commits into
browserbase:mainfrom
aaronjmars:security/fix-href-xss-javascript-url
Open

fix(security): validate URL scheme before rendering href to block javascript: XSS#102
aaronjmars wants to merge 2 commits into
browserbase:mainfrom
aaronjmars:security/fix-href-xss-javascript-url

Conversation

@aaronjmars
Copy link
Copy Markdown

@aaronjmars aaronjmars commented May 10, 2026

Summary

Both compile_report.mjs scripts (event-prospecting + company-research) render attacker-controlled URL fields — LinkedIn / X / GitHub / blog / podcast / image / company website — into <a href="…"> via escapeHtml only. escapeHtml handles &, <, >, " but does not validate the URL scheme, so javascript:fetch(…) (and data:, vbscript:, etc.) survive unchanged and produce a clickable XSS link in the rendered report.

Impact

If an investigator runs event-prospecting against an attacker-controlled conference page (the OSINT-style use case the skill is built for), the page can plant a malicious linkedInProfile / website / image field. The subagent stores the URL verbatim into people/<slug>.md frontmatter, compile_report.mjs renders it into an <a href="javascript:…"> pill, and one click on the LINKEDIN pill of the attacker's own card runs JS in the report origin — exfiltrating the full report DOM (every speaker + every company researched in the run, not just the attacker's own data) to attacker.com via fetch().

file:// origin caps cookie scope, but outbound fetch() still works, and the report is the cross-target leak surface (everything researched in the run is one DOM tree).

Location

7 sinks share the root cause:

  • skills/event-prospecting/scripts/compile_report.mjs:397 — link pills (linkedin/x/github/blog/podcast)
  • skills/event-prospecting/scripts/compile_report.mjs:411 — per-card photo <img src>
  • skills/event-prospecting/scripts/compile_report.mjs:582 — grouped-by-company website link
  • skills/event-prospecting/scripts/compile_report.mjs:673 — companies-table website cell
  • skills/event-prospecting/scripts/compile_report.mjs:680 — attendee LinkedIn cell
  • skills/event-prospecting/scripts/compile_report.mjs:824 — per-company detail page website
  • skills/company-research/scripts/compile_report.mjs:218 — companies-table website cell
  • skills/company-research/scripts/compile_report.mjs:303 — per-company detail page website

(Line numbers post-patch — the originals were a few lines lower in each block.)

Fix

A small safeUrl(u) helper in each file. Allows http://, https://, mailto:, and protocol-relative //; rejects anything that parses as a URL with any other scheme by anchoring against [a-z][a-z0-9+.-]*:. Plain strings without a scheme pass through unchanged so relative paths still render. The helper is applied at every URL-bearing href / src sink before escapeHtml, and target="_blank" links pick up rel="noopener" consistently.

The safeUrl regex is intentionally allow-list-then-reject rather than a single regex — easier to extend (e.g. add tel: if needed) and the early-exit on plain strings keeps callers simple.

function safeUrl(u) {
  if (!u || typeof u !== 'string') return null;
  const trimmed = u.trim();
  if (/^(\/\/|https?:\/\/|mailto:)/i.test(trimmed)) return trimmed;
  if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return null;
  return trimmed;
}

+43 / -13 across the two files; no new dependencies, no behavior change for legitimate http(s) links.

Detected by

Aeon — manual review of the report-template URL sinks. Severity: medium (CWE-79). Picked up that escapeHtml doesn't touch : so any <scheme>:<payload> ride-along survives the encode.

Scanners on this run: semgrep=ok (no findings on the patched files; the 3 raw-html-format warnings on lines 426/427/430 are false positives — the values are already escapeHtml-wrapped or escapeAttr-wrapped); trufflehog=ok (filesystem 246 chunks + git history 740 chunks, 0 verified secrets); osv-scanner=ok (35 transitive CVEs across skills/safe-browser/templates/.../package-lock.json, skills/autobrowse/package-lock.json, skills/cookie-sync/package-lock.json — out of scope for this PR; happy to file a separate dep-bump PR if useful).

Verification

  • node --check skills/event-prospecting/scripts/compile_report.mjs — clean.
  • node --check skills/company-research/scripts/compile_report.mjs — clean.
  • Spot-checked safeUrl against the obvious cases: https://example.com → passes through, javascript:alert(1) → null, data:text/html,<script> → null, vbscript:msgbox(1) → null, //cdn.example.com/x.js → passes through, mailto:hi@example.com → passes through, relative/path.html → passes through, "" / null / undefined → null.
  • No test suite present in the repo for the report-render path, so verification is static.

Disclosure note

The repo doesn't have a SECURITY.md and PVR is off, so under our fix-PR-first policy a public fix PR is the right channel — diff is the disclosure but the fix lands at the same time. Happy to convert to a private advisory if maintainers prefer.


Filed by Aeon.


Note

Medium Risk
Touches HTML generation for multiple attacker-controlled link/image fields; low complexity but could inadvertently drop or alter legitimate non-http(s)/mailto URLs in generated reports.

Overview
Adds a safeUrl() allowlist-based URL sanitizer to both company-research and event-prospecting report compilers, stripping control characters and rejecting non-http(s)/mailto schemes to prevent javascript:-style XSS in generated HTML.

Applies safeUrl() to all rendered href/src sinks (company websites, social link pills, attendee LinkedIn links, and person images) and consistently adds rel="noopener" on target="_blank" links.

Reviewed by Cursor Bugbot for commit 39ac3ed. Bugbot is set up for automated code reviews on this repo. Configure here.

The two `compile_report.mjs` scripts (`event-prospecting` +
`company-research`) render attacker-controlled URL fields — LinkedIn /
X / GitHub / blog / podcast / image / company website — into
`<a href="...">` via `escapeHtml` only. `escapeHtml` handles `& < > "`
but does not validate scheme, so a `javascript:fetch(...)` payload
survives unchanged and produces a clickable XSS link in the report.

7 sinks share the root cause — link pills + per-card photo
(`event-prospecting`), companies-table website rows, attendee LinkedIn
links, and per-company detail website rows in both reports.

Threat: an attacker hosts a Next.js conference page with a malicious
`linkedInProfile` / `website` field. The investigator runs the skill,
the subagent stores the URL verbatim into `people/<slug>.md`
frontmatter, `compile_report.mjs` renders it as a clickable href, one
click on the LINKEDIN pill exfiltrates the full report DOM (every
speaker + every company in the run, not just the attacker's own data)
to attacker.com.

Fix: a small `safeUrl(u)` helper in each file. Allows `http://`,
`https://`, `mailto:`, and protocol-relative `//`; rejects anything
that parses as a URL with any other scheme (`javascript:`, `data:`,
`vbscript:`, `file:`, etc.) by anchoring against the
`[a-z][a-z0-9+.-]*:` pattern. Plain strings without a scheme pass
through (relative paths still work).

Filed by Aeon (https://github.com/aaronjmars/aeon-aaron).
Severity: medium (CWE-79). Detected by manual review.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 39b17fe. Configure here.

Comment thread skills/event-prospecting/scripts/compile_report.mjs
Bugbot follow-up: java\tscript:alert(1) bypassed the scheme regex because
[a-z0-9+.-] rejects the tab — falls through to return trimmed. Browsers
strip C0 controls per WHATWG URL spec before parsing the scheme, so the
javascript: payload still executes. Strip them up front in both safeUrl
implementations (event-prospecting + company-research).
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