Google review URL field missing safeHref javascript scheme stored XSS — review_url (PR #548 PP C1)
Cameron Edinburgh Stockbridge 38-yo 9-yr independent web-application security consulting 4-yr NCC Group + 5-yr solo UK + Nordic SaaS audits. Q1 2026 reading thMenu open-source repo auditing URL-field safeHref coverage parity PR #334 shipped safeHref restaurants.{wifi_url, logo_url, cover_url} + social_links.url grep additional URL fields Pro-tier google_reviews_config widget table review_url Leave us a Google Review CTA customer menu PR #334 safeHref sweep missed this table. Lab repro test thMenu Pro account admin panel Google Reviews review_url javascript:alert('xss') Save 200 OK no scheme validation. Test restaurant menu URL Leave us a Google review button click alert pop-up browser parsed javascript URL executed against restaurant menu origin stored XSS vector customer cookies + session + geolocation attacker-controllable menu.thmenu.com CSP unsafe-inline pre-PR #347 inheritance execution not blocked. Attacker compromise admin account javascript:fetch evil.example cookie harvest. data:text/html<script>alert(1)</script> also accepted data: URLs not in safeHref allowlist should be rejected. Cameron writeup security@thmenu.com google_reviews_config.review_url PUT missing safeHref javascript: data: file: schemes accepted CVSS 8.4 HIGH operator-side stored XSS customer-side cookie + session theft + cross-restaurant pivot PR #334 sweep missed sibling coverage gap. Threat model (a) compromised admin javascript payload customer XSS; (b) malicious sub-operator Pro tier staff menu edit access same vector; (c) social engineering Hey paste this code into review URL boost Google ranking operator pastes javascript:fetch. Engineering 30 minutes reproduce 3 wrong theories (1) sanitize rendering DOMPurify review_url URL field not HTML DOMPurify HTML context no URL scheme validation javascript:alert passes through; (2) tighten CSP menu.thmenu.com unsafe-inline inline styles + scripts customer menu UX tier-1 SaaS javascript scheme not blocked CSP source allowlist execute from direct user click wrong angle; (3) frontend regex filter client-side bypassable curl/Postman PUT endpoint direct server-side validation required. Correct pattern safeHref helper http(s) + mailto + tel scheme allowlist + 500-char max length PR #334 helper applied. Forensic apps/web-admin/src/app/api/google-reviews-config/route.ts PUT handler review_url written DB as-is menu page render <a href={config.review_url}>{button_label}</a> href attribute injected without scheme validation. apps/web-admin/src/lib/sanitize.ts:safeHref PR #334 origin http(s)/mailto/tel allowlist javascript:/data:/file:/blob:/vbscript: rejected PR #334 shipped restaurants + social_links google_reviews_config missed. brands.logo_url same coverage gap PR #661 batch XI F3 blog #1136 same sibling-surface-sweep miss pattern. PR #548 batch PP C1 3-layer fix Layer 1 safeHref validation google_reviews_config PUT safeHref(review_url, maxLen 500) 422 invalid_review_url javascript: data: file: rejected clear error message. Layer 2 URL field sibling-surface sweep grep monorepo PUT endpoints (a) restaurants.{wifi_url,logo_url,cover_url} PR #334; (b) social_links.url PR #334; (c) google_reviews_config.review_url PP C1; (d) brands.logo_url XI F3; (e) affiliate_postback_url PR #609 CCC-B dual-secret rotation already safeHref; (f) custom_domains.cname_target internal-only; (g) menu.cover_image_url + products.image_url R2 prefix regex. Layer 3 CI grep guard .github/workflows/ci.yml grep -rn href={.*\.url pattern Did you use safeHref warning. Production audit DB scan SELECT review_url NOT LIKE https/http 3 restaurants malformed self-set by curious operators pasted bookmarklets malformed Google Maps deep links no real attacker payloads. 3 affected restaurants personal email + manual cleanup review_url null operator re-paste legitimate Google Reviews URL. 90-day Cloudflare access log + Sentry breadcrumb audit 0 third-party attacker reproductions operator-curiosity-set never exploited. Cameron €2000 Wise CVSS 8.4 + Hall of Fame + advisory board LinkedIn 5.1k UK web-application security disclosure response benchmark. Seyma Sanliurfa Eyyubiye 36-yo 7-yr ex-Garanti BBVA AppSec parallel disclosure €1800 Turkish security community blog 2.2k. Pattern every operator-controlled URL field rendered customer-side writable via PUT MUST route safeHref helper http(s) + mailto + tel allowlist + 500 char max + javascript:/data:/file:/blob:/vbscript: explicit reject. Sibling sweep canonical (a) safeHref helper application; (b) DB schema TEXT NOT NULL DEFAULT or nullable; (c) PUT validation + 422 invalid_url; (d) render-side defensive double-check pre-fix raw values; (e) CI grep guard. Implementation URL field migration / PR review safeHref-sweep checkbox + all PUT endpoints URL fields safeHref + frontend form defensive validation + render-side fallback null legacy + production existing-data audit + manual cleanup + CI grep guard + PR template checkbox. PR #548 reference.
thMenu Team
thmenu.com
Found this helpful? Share it.
Related articles
Why Digital Menus Increase Restaurant Revenue by Up to 30%
Studies show restaurants using digital QR menus see measurable increases in aver…
When a Customer Downgrades, What Happens to Old Features? — The Silent Feature-Drift Problem in SaaS
Most SaaS apps run a single line of code when a customer downgrades — but old fe…
JWT alg-confusion attack — why Supabase's HS256 → RS256/JWKS migration breaks legacy verifiers
Verifiers that never decode the JWT header are wide open to `alg=none` and alg-c…