Bootcamp
A guided 60-minute bootcamp for spotting and fixing XSS in your codebase. Run through the steps in order on a real branch; by the end you will have audited a feature, fixed the bugs, and shipped a CSP that prevents the next class of bugs from reaching production.
A 60-minute XSS bootcamp on a real codebase
EXAMPLE
# ===== Bootcamp objectives =====
# 1. Audit one feature for XSS sinks
# 2. Fix every bug found
# 3. Ship a Content-Security-Policy that hardens defaults
# ===== 0–5 min: scope the audit =====
# Pick ONE feature (search results page, comment thread, product review).
# Identify all input + output points.
# A feature with auth + UGC is the highest-yield target.
# ===== 5–25 min: find the sinks =====
# A 'sink' is a place where data lands in the DOM or HTML response.
# Grep for them in priority order:
# JavaScript sinks
git grep -nE "\\.(innerHTML|outerHTML|insertAdjacentHTML)\\s*="
git grep -nE "document\\.write\\("
git grep -nE "eval\\(|new Function\\("
git grep -n 'dangerouslySetInnerHTML' # React
git grep -nE 'v-html' # Vue
git grep -nE 'bypassSecurityTrust' # Angular
# Server-side sinks
git grep -nE '\$_GET|\$_POST|\$_REQUEST' -- '*.php' # raw superglobals
git grep -n '\{\!\! ' # Blade raw echo
git grep -nE 'Markup\\(' -- '*.py' # Jinja2 Markup escape-hatch
git grep -n 'innerText' -- '*.html' # often used WHERE textContent is right
# For each hit, ask: 'where does this value come from?' Trace upstream.
# If ANY part of the chain is user-controlled and not context-correct
# escaped, flag it for fix.
# ===== 25–45 min: fix the bugs =====
# Apply the right encoding for each output context.
# HTML body / text context:
# PHP: echo htmlspecialchars($v, ENT_QUOTES | ENT_HTML5, 'UTF-8');
# Blade: {{ $v }} (NOT {!! !!})
# React: {value} (NOT dangerouslySetInnerHTML)
# Vue: {{ value }} (NOT v-html)
# HTML attribute context:
# PHP: htmlspecialchars($v, ENT_QUOTES, 'UTF-8') with quoted attribute
# React: attr={value} (React escapes attrs automatically)
# JavaScript string literal:
# PHP: json_encode($v, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT)
# JS: never inject server data into <script>; use data-* attributes
# URL parameter:
# PHP: urlencode($v)
# JS: encodeURIComponent(v)
# If you must allow SOME HTML (rich text comments), sanitise:
# PHP: HTMLPurifier
# JS: DOMPurify.sanitize(html, { USE_PROFILES: { html: true } })
# ===== 45–55 min: ship a Content-Security-Policy =====
# CSP is the safety net for the bugs you will write next.
# Start STRICT and relax only if monitoring tells you to.
# Response header (Laravel middleware shown):
# class CspMiddleware {
# public function handle($req, $next) {
# $nonce = base64_encode(random_bytes(16));
# view()->share('cspNonce', $nonce);
# $res = $next($req);
# $res->headers->set('Content-Security-Policy',
# "default-src 'self'; " .
# "script-src 'self' 'nonce-$nonce'; " .
# "style-src 'self' 'nonce-$nonce'; " .
# "img-src 'self' data: https:; " .
# "font-src 'self' data:; " .
# "connect-src 'self' https://api.example.com; " .
# "frame-ancestors 'none'; " .
# "base-uri 'self'; " .
# "object-src 'none'; " .
# "require-trusted-types-for 'script';"
# );
# return $res;
# }
# }
# In templates:
# <script nonce="{{ $cspNonce }}">...</script>
# ===== 55–60 min: enforce in CI =====
# Add a CSP-report-uri / report-to endpoint to monitor violations.
# Run an XSS-focused SAST rule (see the 'xss/sast' lesson) on every PR.
# Mark CSP violations from production as P2 tickets — they often surface
# the next bug class before it becomes a customer report.
Why it matters
A nonce-based CSP makes most XSS bugs unexploitable — even if attacker-controlled HTML lands on the page, the browser refuses to execute injected `