iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…