Lint / no-inner-html
Add XSS lint rules to CI to stop the bug class from landing. Semgrep (the OWASP rules), ESLint security plugins, and framework-specific linters catch the common sinks before code review. Lint catches what reviewers miss; review catches what lint misses.
Semgrep + ESLint + framework linters
EXAMPLE
# ===== 1) Semgrep — language-agnostic =====
# Install
brew install semgrep # or pip install semgrep
# Run the OWASP Top 10 ruleset locally
semgrep --config p/owasp-top-ten
# In CI (.github/workflows/semgrep.yml)
# name: semgrep
# on: [pull_request]
# jobs:
# semgrep:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: returntocorp/semgrep-action@v1
# with:
# config: >-
# p/owasp-top-ten
# p/javascript
# p/php
# p/python
# Custom Semgrep rule — flag dangerouslySetInnerHTML
# .semgrep/no-dangerous-inner-html.yaml
# rules:
# - id: react-dangerously-set-inner-html
# message: 'dangerouslySetInnerHTML accepts untrusted HTML; sanitise with DOMPurify.'
# severity: WARNING
# languages: [tsx, jsx]
# patterns:
# - pattern: <$EL dangerouslySetInnerHTML={...} />
# Custom rule — flag innerHTML assignment from a variable
# rules:
# - id: dom-innerhtml-from-var
# message: 'innerHTML = user-controlled string. Use textContent or sanitise.'
# severity: ERROR
# languages: [javascript, typescript]
# patterns:
# - pattern: $EL.innerHTML = $RHS
# - metavariable-pattern:
# metavariable: $RHS
# pattern-either:
# - pattern: location.$X
# - pattern: document.URL
# - pattern: $Y.value
# - pattern: $Y.textContent
# ===== 2) ESLint — JavaScript / TypeScript / React =====
# npm i -D eslint-plugin-security eslint-plugin-no-unsanitized
# .eslintrc.json
# {
# "plugins": ["security", "no-unsanitized"],
# "extends": [
# "plugin:security/recommended",
# "plugin:no-unsanitized/recommended"
# ]
# }
# Now eslint flags:
# - direct innerHTML / outerHTML / insertAdjacentHTML
# - document.write / eval / Function string
# - target='_blank' without rel='noopener noreferrer'
# ===== 3) Framework linters =====
# Angular template lint
# tslint-angular / angular-eslint flag bypassSecurityTrustHtml/Url
# Vue ESLint
# npm i -D eslint-plugin-vue
# 'vue/no-v-html' rule warns on v-html
# Svelte
# npm i -D eslint-plugin-svelte
# warns on @html when input is untrusted
# Blade (Laravel)
# - Custom Semgrep rule on {!! !!}
# - PHP CS Fixer / Larastan custom rules for safe-by-default echoes
# ===== 4) Browser-side enforcement (defence in depth) =====
# Content-Security-Policy header (see xss/cheatsheet)
# Trusted Types (Chromium browsers):
# require-trusted-types-for 'script'
# trusted-types default
# Sanitiser API: DOMPurify only via Trusted Types policy
# ===== 5) Integration with CI =====
# Make the rules BLOCK on PR
# - Semgrep ERROR -> exit 1
# - ESLint error -> npm run lint exits non-zero
# - Required check on PR via branch protection
# Allow opt-out with justification
# // semgrep-ignore: dom-innerhtml-from-var — sanitised by DOMPurify above
# // eslint-disable-next-line no-unsanitized/property -- rendered HTML from CMS
# ===== 6) Inventory + suppression review =====
# Once a quarter:
# - List all suppression comments in the codebase
# git grep -nE 'semgrep-ignore|eslint-disable.*no-unsanitized|nosemgrep'
# - Verify each is still justified
# - Treat permanent suppressions as tech debt
# ===== 7) IDE feedback =====
# - Semgrep VS Code extension
# - ESLint VS Code extension
# - Auto-fix on save where possible
# ===== Pitfalls =====
# - Lint-blocking on warnings -> teams add 'eslint-disable' indiscriminately
# - Lint without educational error messages -> author ignores rule
# - Skipping framework-specific linters (vue / svelte / blade) -> coverage gap
# - Only running in CI; not in editor -> feedback loop too slow
Why it matters
Run a Semgrep ruleset + ESLint with security plugins on every PR, with clear suppression comments allowed but inventoried quarterly. Lint catches what reviewers miss; reviewers catch what lint misses; together they make the XSS bug class progressively harder to land.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// eslint-plugin-react + eslint-plugin-security: // react/no-danger // security/detect-no-csrf-before-method-override // In TypeScript: prefer strict types + readonly DOM nodes.Try it Yourself »
Discussion
Loading…