fix(security): validate URL scheme before rendering href to block javascript: XSS#102
Open
aaronjmars wants to merge 2 commits into
Open
fix(security): validate URL scheme before rendering href to block javascript: XSS#102aaronjmars wants to merge 2 commits into
aaronjmars wants to merge 2 commits into
Conversation
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
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).
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.

Summary
Both
compile_report.mjsscripts (event-prospecting+company-research) render attacker-controlled URL fields — LinkedIn / X / GitHub / blog / podcast / image / company website — into<a href="…">viaescapeHtmlonly.escapeHtmlhandles&,<,>,"but does not validate the URL scheme, sojavascript:fetch(…)(anddata:,vbscript:, etc.) survive unchanged and produce a clickable XSS link in the rendered report.Impact
If an investigator runs
event-prospectingagainst an attacker-controlled conference page (the OSINT-style use case the skill is built for), the page can plant a maliciouslinkedInProfile/website/imagefield. The subagent stores the URL verbatim intopeople/<slug>.mdfrontmatter,compile_report.mjsrenders 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 viafetch().file://origin caps cookie scope, but outboundfetch()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-cardphoto<img src>skills/event-prospecting/scripts/compile_report.mjs:582— grouped-by-company website linkskills/event-prospecting/scripts/compile_report.mjs:673— companies-table website cellskills/event-prospecting/scripts/compile_report.mjs:680— attendee LinkedIn cellskills/event-prospecting/scripts/compile_report.mjs:824— per-company detail page websiteskills/company-research/scripts/compile_report.mjs:218— companies-table website cellskills/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. Allowshttp://,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 beforeescapeHtml, andtarget="_blank"links pick uprel="noopener"consistently.The
safeUrlregex is intentionally allow-list-then-reject rather than a single regex — easier to extend (e.g. addtel:if needed) and the early-exit on plain strings keeps callers simple.+43 / -13across the two files; no new dependencies, no behavior change for legitimatehttp(s)links.Detected by
Aeon — manual review of the report-template URL sinks. Severity: medium (CWE-79). Picked up that
escapeHtmldoesn't touch:so any<scheme>:<payload>ride-along survives the encode.Scanners on this run:
semgrep=ok(no findings on the patched files; the 3raw-html-formatwarnings on lines 426/427/430 are false positives — the values are alreadyescapeHtml-wrapped orescapeAttr-wrapped);trufflehog=ok(filesystem 246 chunks + git history 740 chunks, 0 verified secrets);osv-scanner=ok(35 transitive CVEs acrossskills/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.safeUrlagainst 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.Disclosure note
The repo doesn't have a
SECURITY.mdand 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 bothcompany-researchandevent-prospectingreport compilers, stripping control characters and rejecting non-http(s)/mailtoschemes to preventjavascript:-style XSS in generated HTML.Applies
safeUrl()to all renderedhref/srcsinks (company websites, social link pills, attendee LinkedIn links, and person images) and consistently addsrel="noopener"ontarget="_blank"links.Reviewed by Cursor Bugbot for commit 39ac3ed. Bugbot is set up for automated code reviews on this repo. Configure here.