Skip to content

Escape third-party data in map tooltips/popups; lazy popups - #9

Merged
DeanCron merged 2 commits into
mainfrom
deancron-ci-tests-and-runtime-upgrade
Oct 6, 2026
Merged

DeanCron merged 2 commits into
mainfrom
deancron-ci-tests-and-runtime-upgrade

Conversation

@DeanCron

@DeanCron DeanCron commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Security fix: stored XSS in map tooltips and popups

Leaflet sets string tooltip and popup content with innerHTML. Many layers inserted third-party fields without escaping. Affected sources: OSM (airport, transit stop, crowd magnet and railroad names, camera manufacturer), Google Places (Costco and EMS names and addresses), EPA (Superfund and TRI facility names, addresses, URLs), FEMA (flood and tornado labels) and HIFLD (power line owner and voltage class).

The CSP allows script-src 'unsafe-inline'. A crafted value such as an OSM name of <img src=x onerror=…> would therefore run in the app's origin.

Changes

  • Every external value now goes through the shared escapeHtml in src/utils/html.ts. The two duplicate local escapers in MapPage.tsx are removed.
  • New safeHttpUrl(): popup links (the EPA Superfund URL and the TRI facility report) accept only http:/https: URLs and are attribute-escaped. Before this, a javascript: URL would have been rendered as-is.
  • cameraPopup escapes the manufacturer label and URL-encodes the node id. transitPopup escapes the stop name.
  • Regression tests: src/utils/html.test.ts and src/map/popupEscaping.test.ts.

Perf: lazy popups (separate commit)

bindPopup(() => html) builds popup HTML only when a user opens a popup, not for every marker added while panning. Every captured value is a per-iteration const.

Verification

tsc -b, npm run lint, npm test (218 tests) and npm run build (bundle budget) all pass.

Follow-up suggestion

Tighten the CSP by removing 'unsafe-inline' from script-src. The GA inline snippet would need a hash or nonce.

Dean Cron and others added 2 commits October 6, 2026 18:06
Leaflet renders string tooltip/popup content via innerHTML, and many map
layers interpolated OSM, Google Places, EPA, FEMA, and HIFLD fields
unescaped. With the CSP allowing 'unsafe-inline' scripts, a crafted name
(e.g. <img onerror>) would execute in the app origin.

- Route every external value through the shared escapeHtml and drop the
  two duplicate local escapers in MapPage.
- Add safeHttpUrl so popup links only accept http(s) URLs (EPA superfund
  and TRI facility links were previously unvalidated).
- Escape camera manufacturer/node id and transit stop names.
- Add regression tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Pass a content function to bindPopup so popup HTML for viewport layers is
only generated when a user opens a popup, instead of for every marker.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@DeanCron
DeanCron merged commit fc0a431 into main Oct 6, 2026
2 checks passed
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