Install in seconds
Install this skill
Copy the command and run it in your terminal. You can review the source before installing.
terminal
git clone https://github.com/utkusen/sast-skills

Works with Git. The repository opens in your current directory.

πŸ›‘οΈ
Quality

XSS Detection

by utkusen

Detect Cross-Site Scripting vulnerabilities in a codebase using a three-phase approach: recon for sink sites, batched taint tracing, and result merging. Use when auditing code for XSS bugs.

1.2K stars60 forksAdded 2026/07/20
ai-securityclaudeclaude-codesast

Documentation

README

Cross-Site Scripting (XSS) Detection

You are performing a focused security assessment to find Cross-Site Scripting vulnerabilities in a codebase. This skill uses a three-phase approach with subagents: recon (find sink sites), batched verify (trace taint for parallel batches of up to 3 sinks each), and merge (consolidate batch results into one report).

Prerequisites: sast/architecture.md must exist. Run the analysis skill first if it doesn't.


What is XSS

XSS occurs when user-supplied input is incorporated into a web page's HTML, JavaScript, or DOM without proper escaping or sanitization. This allows attackers to inject and execute arbitrary scripts in victims' browsers, leading to session hijacking, credential theft, defacement, and malware distribution.

The core pattern: unescaped, unsanitized user input reaches an HTML/JS output sink.

XSS Types

  • Reflected XSS: User input is immediately echoed back in the HTTP response (e.g., a search term rendered directly into the page HTML).
  • Stored XSS: User input is saved to persistent storage (database, file) and later rendered in HTML for other users.
  • DOM-based XSS: Client-side JavaScript reads from an attacker-controlled source (location.search, location.hash, document.cookie) and writes to a dangerous DOM sink (innerHTML, eval, document.write) without server involvement.

What XSS IS

Server-side HTML sinks β€” rendering user data into HTML responses without escaping:

  • Python/Jinja2: {{ var | safe }}, {% autoescape off %}...{{ var }}...{% endautoescape %}
  • Python/Django: mark_safe(var), format_html(...) with %s and unescaped input, {{ var | safe }} in templates
  • Python/Flask: Markup(var), render_template_string(f"...{var}...")
  • PHP: echo $var, print $var, <?= $var ?> without htmlspecialchars()
  • Ruby/Rails: raw(var), var.html_safe, <%= raw var %>, content_tag with .html_safe
  • Java/JSP: <%= var %>, ${var} without <c:out> or fn:escapeXml()
  • Java/Thymeleaf: th:utext="${var}" (unescaped), [(${var})]
  • Go/html-template misuse: using template.HTML(var), template.JS(var), template.URL(var) to bypass auto-escaping
  • C#/Razor: @Html.Raw(var), MvcHtmlString.Create(var)
  • Node.js/EJS: <%- var %> (unescaped), vs <%= var %> (safe)
  • Node.js/Handlebars: {{{ var }}} (triple-brace, unescaped)
  • Node.js/Pug: !{var} (unescaped)
  • Express: res.send("<html>..." + var + "..."), res.write("<p>" + var + "</p>")

Client-side DOM sinks β€” JavaScript writing user-controlled data to the DOM unsafely:

  • element.innerHTML = var
  • element.outerHTML = var
  • document.write(var), document.writeln(var)
  • element.insertAdjacentHTML('beforeend', var)
  • jQuery: $(element).html(var), $(element).append(var) (when var contains HTML), $('<div>' + var + '</div>')
  • React: dangerouslySetInnerHTML={{ __html: var }}
  • Angular: [innerHTML]="var", bypassSecurityTrustHtml(var), bypassSecurityTrustScript(var), bypassSecurityTrustUrl(var)
  • Vue: v-html="var"

JavaScript execution sinks β€” user-controlled data evaluated as code:

  • eval(var)
  • setTimeout(var, delay) / setInterval(var, delay) when var is a string
  • new Function(var)()
  • element.setAttribute('onclick', var), element.setAttribute('href', 'javascript:' + var)
  • location.href = var, location.replace(var), location.assign(var) (when var is user-controlled and can be javascript:...)
  • element.src = var, element.action = var (script injection via javascript: URIs)
  • scriptElement.text = var, scriptElement.textContent = var

DOM-based sources β€” attacker-controlled inputs read by client-side JavaScript:

  • location.search (URL query string)
  • location.hash (URL fragment)
  • location.href
  • document.referrer
  • document.URL, document.documentURI
  • document.cookie
  • postMessage event data (event.data)
  • window.name
  • localStorage.getItem(...), sessionStorage.getItem(...) (if populated from URL or postMessage)

What XSS is NOT

Do not flag these as XSS:

  • CSRF: Forging requests on behalf of a user β€” a separate vulnerability class
  • SQLi via XSS: Injecting SQL through an XSS vector β€” the SQL injection itself is the primary finding
  • Clickjacking: Embedding pages in iframes β€” different vulnerability class
  • Header injection: Injecting newlines into HTTP response headers β€” separate class (HTTP Response Splitting)
  • Safe template output: Auto-escaped {{ var }} in Jinja2/Django/Twig/Blade/Handlebars double-brace syntax with auto-escaping on β€” these are safe
  • textContent / innerText: These write plain text only; no HTML parsing occurs β€” safe

Patterns That Prevent XSS

When you see these patterns, the code is likely not vulnerable:

1. Context-aware auto-escaping (most template engines default)

# Jinja2 / Django (auto-escape on by default)
{{ var }}          # HTML-escaped β†’ safe

# EJS
<%= var %>         # HTML-escaped β†’ safe

# Handlebars
{{ var }}          # HTML-escaped β†’ safe

# Pug
= var              # HTML-escaped β†’ safe

# Thymeleaf
th:text="${var}"   # HTML-escaped β†’ safe

# Razor (C#)
@var               # HTML-encoded β†’ safe

2. Explicit escaping before output

// PHP
echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8');
# Rails
<%= h(var) %>
<%= ERB::Util.html_escape(var) %>
// JSP with JSTL
<c:out value="${var}"/>
// or fn:escapeXml()
${fn:escapeXml(var)}
// html/template β€” auto-escapes by context (HTML, JS, URL, CSS)
{{.Var}}   // safe inside html/template

3. DOM manipulation using safe properties

element.textContent = userInput;   // plain text, no HTML parsing β€” safe
element.innerText = userInput;     // plain text β€” safe

4. Sanitization with an allowlisted HTML library

// DOMPurify
element.innerHTML = DOMPurify.sanitize(userInput);

// sanitize-html with strict config
const clean = sanitizeHtml(userInput, { allowedTags: [], allowedAttributes: {} });

5. React / Angular / Vue auto-escaping

// React JSX β€” auto-escaped
return <div>{userInput}</div>;
<!-- Angular β€” auto-escaped -->
<div>{{ userInput }}</div>
<!-- Vue β€” auto-escaped -->
<div>{{ userInput }}</div>

Vulnerable vs. Secure Examples

Python β€” Flask / Jinja2

# VULNERABLE: Markup() bypasses Jinja2 auto-escaping
@app.route('/greet')
def greet():
    name = request.args.get('name', '')
    return render_template_string(f"<h1>Hello, {name}!</h1>")   # raw f-string, no template escaping

# VULNERABLE: mark_safe equivalent
@app.route('/profile')
def profile():
    bio = request.args.get('bio', '')
    return render_template('profile.html', bio=Markup(bio))      # Markup() marks it as safe, bypassing escaping

# SECURE: use template with auto-escaping (never pass Markup around user input)
@app.route('/greet')
def greet():
    name = request.args.get('name', '')
    return render_template('greet.html', name=name)              # template: {{ name }} β€” auto-escaped

Python β€” Django

# VULNERABLE: mark_safe() with user input
def user_bio(request):
    bio = request.GET.get('bio', '')
    safe_bio = mark_safe(bio)   # user input bypasses Django's auto-escaping
    return render(request, 'bio.html', {'bio': safe_bio})

# SECURE: pass raw string; template handles escaping
def user_bio(request):
    bio = request.GET.get('bio', '')
    return render(request, 'bio.html', {'bio': bio})   # template: {{ bio }} β€” auto-escaped

PHP

// VULNERABLE: echo without escaping
function showUsername($username) {
    echo "<p>Welcome, " . $username . "</p>";
}

// SECURE: htmlspecialchars
function showUsername($username) {
    echo "<p>Welcome, " . htmlspecialchars($username, ENT_QUOTES, 'UTF-8') . "</p>";
}

Node.js β€” Express (string concatenation)

// VULNERABLE: user input concatenated into HTML response
app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`<h1>Results for: ${query}</h1>`);
});

// SECURE: use a template engine with auto-escaping, or escape manually
const escapeHtml = require('escape-html');
app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`<h1>Results for: ${escapeHtml(query)}</h1>`);
});

Node.js / EJS

<!-- VULNERABLE: unescaped output -->
<div><%- userInput %></div>

<!-- SECURE: escaped output -->
<div><%= userInput %></div>

Node.js / Handlebars

<!-- VULNERABLE: triple-brace, unescaped -->
<div>{{{ userInput }}}</div>

<!-- SECURE: double-brace, auto-escaped -->
<div>{{ userInput }}</div>

JavaScript β€” DOM Sinks

// VULNERABLE: innerHTML with URL fragment
const name = location.hash.substring(1);
document.getElementById('greeting').innerHTML = 'Hello, ' + name;

// SECURE: textContent
const name = location.hash.substring(1);
document.getElementById('greeting').textContent = 'Hello, ' + name;
// VULNERABLE: eval with postMessage data
window.addEventListener('message', (event) => {
  eval(event.data);
});

// SECURE: parse and validate; never eval postMessage data
window.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  // handle data safely
});

React

// VULNERABLE: dangerouslySetInnerHTML with user input
function Comment({ content }) {
  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}

// SECURE: render as text (auto-escaped by React)
function Comment({ content }) {
  return <div>{content}</div>;
}

Angular

// VULNERABLE: bypassing Angular's DomSanitizer
constructor(private sanitizer: DomSanitizer) {}
getUserHtml(input: string): SafeHtml {
  return this.sanitizer.bypassSecurityTrustHtml(input);  // unsafe if input is user-controlled
}
<!-- VULNERABLE: [innerHTML] with unsanitized value -->
<div [innerHTML]="userInput"></div>

<!-- SECURE: use interpolation (auto-escaped) -->
<div>{{ userInput }}</div>

Ruby on Rails

<%# VULNERABLE: raw() or html_safe with user input %>
<%= raw(@user.bio) %>
<%= @user.bio.html_safe %>

<%# SECURE: default ERB escaping %>
<%= @user.bio %>

Java β€” JSP

<%-- VULNERABLE: scriptlet echo --%>
<p>Hello, <%= request.getParameter("name") %></p>

<%-- VULNERABLE: EL without c:out --%>
<p>Hello, ${param.name}</p>

<%-- SECURE: c:out escaping --%>
<p>Hello, <c:out value="${param.name}"/></p>

Go β€” html/template vs. text/template

// VULNERABLE: using text/template (no HTML escaping)
import "text/template"
tmpl := template.Must(template.New("").Parse("<h1>Hello, {{.Name}}!</h1>"))
tmpl.Execute(w, data)

// VULNERABLE: using template.HTML() cast to bypass escaping
import "html/template"
name := template.HTML(r.URL.Query().Get("name"))   // bypasses auto-escaping

// SECURE: html/template with plain string value
import "html/template"
tmpl := template.Must(template.New("").Parse("<h1>Hello, {{.Name}}!</h1>"))
tmpl.Execute(w, data)   // .Name is a plain string β€” auto-escaped

Execution

This skill runs in three phases using subagents. Pass the contents of sast/architecture.md to all subagents as context.

Phase 1: Find XSS Sink Sites

Launch a subagent with the following instructions:

Goal: Find every location in the codebase where data is rendered into HTML, JavaScript, or the DOM in a way that could allow script injection β€” any unescaped or explicitly-marked-safe output, any dangerous DOM property assignment, any JavaScript execution sink. Write results to sast/xss-recon.md.

Context: You will be given the project's architecture summary. Use it to understand the frontend stack, template engines, server-side rendering frameworks, and any client-side JavaScript patterns.

What to search for β€” vulnerable sink patterns:

Flag ANY dynamic variable passed to a dangerous output sink. You are not yet checking whether the variable is user-controlled β€” that is Phase 2's job.

1. Server-side template unescaped output:

  • Jinja2/Django: {{ var | safe }}, {% autoescape off %}, Markup(var), mark_safe(var), format_html(...) with direct user-controlled format args
  • EJS: <%- var %>
  • Handlebars/Mustache: {{{ var }}}
  • Pug: !{var}
  • Thymeleaf: th:utext="${var}", [(${var})]
  • Twig: {{ var | raw }}
  • Blade (Laravel): {!! $var !!}
  • Rails ERB: raw(var), var.html_safe, <%= raw var %>
  • PHP: echo $var, print $var, <?= $var ?> without htmlspecialchars()
  • Go: template.HTML(var), template.JS(var), template.URL(var), usage of text/template for HTML output
  • C#/Razor: @Html.Raw(var), MvcHtmlString.Create(var)

2. Direct HTML string construction in server-side code:

  • String concatenation or interpolation building an HTML response: res.send("<p>" + var + "</p>"), f"<h1>{var}</h1>", "<div>" + var + "</div>"
  • render_template_string(f"...{var}...") in Flask

3. Client-side DOM sinks:

  • element.innerHTML = var
  • element.outerHTML = var
  • document.write(var), document.writeln(var)
  • element.insertAdjacentHTML(position, var)
  • jQuery: $(el).html(var), $(el).append(var), $('<tag>' + var + '</tag>'), $.parseHTML(var) passed to DOM
  • React: dangerouslySetInnerHTML={{ __html: var }}
  • Angular: [innerHTML]="var", bypassSecurityTrustHtml(var), bypassSecurityTrustScript(var), bypassSecurityTrustUrl(var), bypassSecurityTrustStyle(var), bypassSecurityTrustResourceUrl(var)
  • Vue: v-html="var"

4. JavaScript execution sinks:

  • eval(var)
  • setTimeout(var, ...) / setInterval(var, ...) where var is a string variable (not a function reference)
  • new Function(var)()
  • scriptElement.text = var, scriptElement.textContent = var
  • element.setAttribute('onclick', var), element.setAttribute('href', 'javascript:' + var), and similar event-handler attribute assignments
  • URL-based sinks where javascript: URIs could execute: location.href = var, location.replace(var), element.src = var, element.action = var

5. DOM-based XSS patterns β€” client-side code reading from attacker-controlled sources and passing to any sink above:

  • Reading from: location.search, location.hash, location.href, document.referrer, document.URL, document.cookie, window.name, postMessage handler (event.data), URLSearchParams
  • Then passing to an HTML or JS sink without escaping

What to skip (these are safe output patterns β€” do not flag):

  • Auto-escaped template output: {{ var }} in Jinja2 (auto-escape on), <%= var %> in EJS, {{ var }} in Handlebars double-brace, @var in Razor, th:text in Thymeleaf
  • element.textContent = var and element.innerText = var β€” no HTML parsing, safe
  • React JSX {var} β€” auto-escaped
  • Angular {{ var }} interpolation β€” auto-escaped
  • Vue {{ var }} interpolation β€” auto-escaped
  • DOMPurify.sanitize(var) wrapping an innerHTML assignment β€” typically safe (verify config)
  • sanitize-html, xss, or similar allowlist sanitizer library wrapping output

Output format β€” write to sast/xss-recon.md:

# XSS Recon: [Project Name]

## Summary
Found [N] locations where data is rendered into HTML/JS/DOM without guaranteed escaping.

## Sink Sites

### 1. [Descriptive name β€” e.g., "innerHTML assignment in search results handler"]
- **File**: `path/to/file.ext` (lines X-Y)
- **Function / endpoint / component**: [function name, route, or component]
- **Sink type**: [server-side template / HTML string concat / DOM innerHTML / eval / JS execution sink / DOM-based source-to-sink]
- **Sink call**: [the exact API or property used β€” e.g., `innerHTML`, `mark_safe()`, `<%- %>`]
- **Interpolated variable(s)**: `var_name` β€” [brief note, e.g., "unknown origin" or "looks like user profile field"]
- **XSS type**: [Reflected / Stored / DOM-based β€” best guess at this stage]
- **Code snippet**:

[the vulnerable sink code]


[Repeat for each site]

After Phase 1: Check for Candidates Before Proceeding

After Phase 1 completes, read sast/xss-recon.md. If the recon found zero sink sites (the summary reports "Found 0" or the "Sink Sites" section is empty or absent), skip Phase 2 and Phase 3 entirely. Instead, write the following content to sast/xss-results.md and stop (you may delete sast/xss-recon.md after writing):

# XSS Analysis Results

No vulnerabilities found.

Only proceed to Phase 2 if Phase 1 found at least one sink site.

Phase 2: Verify β€” Trace User Input to Sinks (Batched)

After Phase 1 completes, read sast/xss-recon.md and split the sink sites into batches of up to 3 sink sites each. Launch one subagent per batch in parallel. Each subagent traces taint only for its assigned sinks and writes results to its own batch file.

Batching procedure (you, the orchestrator, do this β€” not a subagent):

  1. Read sast/xss-recon.md and count the numbered sink sections (### 1., ### 2., etc.).
  2. Divide them into batches of up to 3. For example, 8 sinks β†’ 3 batches (1-3, 4-6, 7-8).
  3. For each batch, extract the full text of those sink sections from the recon file.
  4. Launch all batch subagents in parallel, passing each one only its assigned sinks.
  5. Each subagent writes to sast/xss-batch-N.md where N is the 1-based batch number.
  6. Identify the project's primary language/framework from sast/architecture.md and select only the matching examples from the "Vulnerable vs. Secure Examples" section above. For example, if the project uses React with an Express API, include the relevant Node.js and React examples. Include these selected examples in each subagent's instructions where indicated by [TECH-STACK EXAMPLES] below.

Give each batch subagent the following instructions (substitute the batch-specific values):

Goal: For each assigned XSS sink site, determine whether a user-supplied value reaches the output variable. Write results to sast/xss-batch-[N].md.

Your assigned sink sites (from the recon phase):

[Paste the full text of the assigned sink sections here, preserving the original numbering]

Context: You will be given the project's architecture summary. Use it to understand request entry points, data flows, middleware, and client-side data sources.

For each sink site, trace the interpolated variable(s) backwards to their origin:

User-controlled sources to look for:

  1. HTTP request sources (server-side):
    • Query parameters: request.GET.get(...), req.query.x, params[:x], $_GET['x'], c.Query("x"), r.URL.Query().Get("x")
    • Path parameters: request.path_params['id'], req.params.id, params[:id], $_GET['id']
    • Request body / form fields: request.POST.get(...), req.body.x, request.form.get(...), $_POST['x']
    • HTTP headers: request.headers.get(...), req.headers['x'], $_SERVER['HTTP_X_CUSTOM']
    • Cookies: request.COOKIES.get(...), req.cookies.x, $_COOKIE['x']
    • File upload filenames or content: request.files['x'].filename
  2. Attacker-controlled DOM sources (client-side / DOM-based XSS):
    • location.search, location.hash, location.href, document.referrer, document.URL
    • window.name, document.cookie
    • postMessage event: window.addEventListener('message', (e) => { ... e.data ... })
    • URLSearchParams values derived from location.search
    • localStorage / sessionStorage values written from URL or postMessage
  3. Stored (second-order) input β€” the variable is read from persistent storage (database, file, cache), but the stored value or

More from utkusen

Other Claude Code skills by this author in the directory.

πŸ”
1w ago

SAST Analysis

Performs reconnaissance and architecture mapping as the first phase of a security assessment. Analyzes tech stack, entry points, data flows, and trust boundaries to prepare for vulnerability detection.
AI Engineering
+1%1.2K60
πŸ”Ž
1w ago

Business Logic Vulnerability Detection

Detects business logic vulnerabilities such as price manipulation, workflow bypass, race conditions, and reward abuse using a three-phase threat modeling approach. Use for security audits requiring logic flaw analysis.
Quality
+1%1.2K60
πŸ”
1w ago

Insecure File Upload Detection

Detects insecure file upload vulnerabilities in a codebase through a three-phase approach: discovery of upload endpoints, batched verification of bypass vectors, and consolidation of findings. Use when auditing for file upload, unrestricted upload, or extension bypass bugs.
Quality
+1%1.2K60
πŸ”
1w ago

GraphQL Injection Detection

Detects GraphQL injection vulnerabilities in codebases through a phased static analysis: recon for unsafe document assembly, batched verification of user-input traces, and merging results. Use when auditing GraphQL APIs for injection risks.
Quality
+1%1.2K60
πŸ”‘
1w ago

Hardcoded Secrets Detection

Detect hardcoded API keys, tokens, passwords, and other sensitive data in publicly accessible code such as frontend JavaScript, mobile apps, and HTML templates. Uses a three-phase approach with batch verification.
Quality
+1%1.2K60
πŸ”Ž
1w ago

IDOR Detection

Detects IDOR vulnerabilities by analyzing endpoints for missing ownership or authorization checks. Uses a three-phase parallel subagent approach for efficient verification. Requires a prior SAST run (sast/architecture.md).
Quality
+1%1.2K60