A regex "sanitiser", a GET delete link, and a CSP that permits everything inline
A community site lets members comment on articles. Comments are stored as raw HTML so people can use <b> and <a>. On render, the server applies html.replace(/<script[\s\S]*?<\/script>/gi, '') plus a strip of on\w+= attributes, then writes the result into the page with innerHTML. Comments are also rendered into a moderator's queue. Moderators delete via <a href="/admin/comments/42/delete">Delete</a>, protected only by the session cookie. That cookie is set from JavaScript as document.cookie = 'sid=' + token with no attributes, and the site is served over HTTPS. Last sprint a CSP was deployed as Content-Security-Policy: script-src 'self' 'unsafe-inline'. A researcher posted a comment containing an <img> element with an error handler; it executed inside a moderator's session and 30 comments were deleted. The team's proposed fix is to extend the regex to also block onerror and <img>.
1. Explain why the regex approach cannot work, describe the classes of bypass it misses, and specify the correct filtering architecture.
Read the worked answer
The regex is being asked to parse a context-free grammar with a tool that has no grammar. HTML has quoting rules, character references, comments, CDATA-like sections, foreign content (SVG/MathML), null bytes, and attribute separators that are not whitespace - a pattern cannot enumerate all of them, and every enumeration you add is another patch to a moving target. The proposed fix (block <img> and onerror) fails immediately to onload, onfocus with autofocus, onanimationend, <svg><a xlink:href="javascript:...">, <math><mtext><script> style nesting, <a href="jav	ascript:...">, or an event handler split across oddly quoted attributes that the on\w+= pattern reads differently from the browser.
Correct architecture, in order of preference:
- Do not accept HTML. Store plain text and render with
textContent. If bold and links are the only requirement, use a constrained markdown subset rendered server-side into an allowlisted tree. - If HTML must be accepted, parse it with a real parser (an HTML5-compliant library) and rebuild the output from an allowlist: permitted element names, permitted attributes per element, URL schemes restricted to
http,https,mailtowithrel="noopener noreferrer"on links, and all other nodes dropped or text-escaped. Mature sanitisers (DOMPurify, OWASP Java HTML Sanitizer, Bleach) exist; do not hand-roll. - Sanitise on input and again on output, because the stored corpus contains three weeks of hostile content and because rendering contexts change. Encode at the sink as the final defence:
innerHTMLshould be replaced withtextContentor DOM building wherever the content is not deliberately allowed HTML.
It is also worth noting the researcher did not need to break the regex cleverly - the on\w+= strip runs after the script strip and does not understand attribute boundaries, so a handler inside a quoted attribute value survives.
2. Evaluate the deployed CSP. What would you change, and where does CSP sit relative to sanitisation in your defence model?
Read the worked answer
The policy as written provides almost nothing. 'unsafe-inline' in script-src re-enables inline <script> blocks, inline event-handler attributes, and javascript: URLs - the exact vectors in play - and when a nonce or hash is also present, browsers ignore 'unsafe-inline' in supporting browsers, but here there is neither. script-src 'self' also matters less than people think, because 'self' permits any script served from the origin, so an uploaded file or a JSON endpoint misinterpreted as script can execute.
Better target policy:
script-src 'nonce-<random>' 'strict-dynamic'with a fresh nonce per response, and no'unsafe-inline', no'unsafe-eval'.object-src 'none',base-uri 'none'(blocks<base>hijacking of relative URLs),frame-ancestors 'none'(clickjacking),form-action 'self'.default-src 'self'with explicitimg-src,style-src(prefer hashes over'unsafe-inline'),connect-srclimited to the APIs actually called.- Consider Trusted Types (
require-trusted-types-for 'script') - it makes assigning strings toinnerHTMLthrow, which would have stopped this bug at the sink. - Roll out with
Content-Security-Policy-Report-Onlyplus areport-uri/report-toendpoint first, so you find violations before you break the site.
Position: CSP is a mitigation layer, not a fix. It limits what an injected script can do if it executes, but it cannot stop markup injection itself, cannot stop data exfiltration through permitted channels such as img-src if that is broad, and does nothing for the server-side consequence of the request. Sanitisation and output encoding must remove the injection; CSP is what saves you when a sanitiser has a zero-day. Ship both, and treat the CSP as an alarm as well as a control.
3. Fix the session cookie and the delete action. Explain precisely what `SameSite=Lax` does and does not protect against here.
Read the worked answer
Cookie. Set it server-side with Set-Cookie: __Host-sid=<value>; Path=/; Secure; HttpOnly; SameSite=Lax (or Strict for the admin surface). Requirements and reasoning: HttpOnly makes it unreadable from JavaScript, so an XSS can still act as the user through the browser but cannot exfiltrate the token to be replayed later from elsewhere. Secure prevents it being sent over plain HTTP and blocks most network-level leakage. __Host- forces Secure, an origin-wide Path=/, and no Domain attribute, which prevents a compromised sibling subdomain from setting a cookie for your host. Setting cookies from document.cookie cannot apply HttpOnly at all, which alone is reason to move it to the server. Add Max-Age/Expires and rotate the session id on privilege change and at login.
Delete action. SameSite=Lax sends the cookie on top-level GET navigations from other sites, and only withholds it from cross-site subresource requests and cross-site POSTs. A delete implemented as a GET link is therefore fully exploitable: an attacker page that navigates the moderator to /admin/comments/42/delete carries the cookie and performs the action. SameSite=Strict would block that particular navigation but breaks ordinary inbound links and is not a substitute for a token.
The fix is to change state with POST (or DELETE) only, never GET, and to require an anti-CSRF token: a per-session random value stored server-side, embedded in the form as a hidden field or sent as a header, compared in constant time on the server. Add an explicit confirmation step for bulk actions, and consider Origin/Referer checking as a secondary signal. Then apply SameSite=Lax as defence in depth, and make the endpoint idempotent-safe so a duplicated request cannot delete twice beyond the first no-op. Also verify authorisation per object - that the comment belongs to a resource this moderator may moderate - so the endpoint is not relying on obscurity of the id.