# Static Site Security Best Practices Report
## 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 vulnerabilities
- `npm audit fix` - Automatically install compatible security updates
- `npm 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:**
```bash
# In CI/CD pipelines
npm ci --only=production
# Avoid in production
npm 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:
```yaml
# Example GitHub Actions workflow
- name: Security Audit
run: npm audit --audit-level=high
```
**Audit Levels:**
- `low` - All vulnerabilities
- `moderate` - Moderate and above
- `high` - High and critical only
- `critical` - 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 --force` as 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.
```json
// package.json - Add this to .npmrc for team consistency
{
"engines": {
"npm": ">=8.0.0"
}
}
```
**Lock File Security:**
- Lock files prevent unexpected version changes via MITM attacks
- They include integrity checksums (SHA-512) for each package
- Inconsistencies between `package.json` and 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:
```json
// Use overrides in package.json (npm 8.3+)
{
"overrides": {
"vulnerable-package": "^3.0.0"
}
}
```
**Alternative strategies:**
- Switch to well-maintained alternatives
- Fork and maintain internally if necessary
- Use tools like `npm-check-updates` to 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:**
```bash
npm 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:**
```javascript
// DANGEROUS - User input in exec
const { exec } = require('child_process');
const userInput = req.query.filename; // e.g., "file.txt; rm -rf /"
exec(`cat ${userInput}`, callback); // VULNERABLE!
// Attack chains using: ; & | || $() < > >>
```
### Secure Alternatives and Best Practices
#### 1. Use execFile Instead of exec/execSync
**Recommended Approach:**
```javascript
const { execFile } = require('child_process');
// SAFE - execFile does not spawn a shell
execFile('ls', ['-lh', userProvidedPath], (error, stdout) => {
if (error) {
console.error('Error:', error);
return;
}
console.log(stdout);
});
```
`execFile` helps prevent arbitrary shell commands from being executed and is the recommended defense.
#### 2. Use spawn with Arguments Array
```javascript
const { spawn } = require('child_process');
// SAFE - Arguments passed as array, not string
const child = spawn('grep', [userInput], {
cwd: '/safe/directory',
env: { PATH: '/usr/bin' } // Restricted PATH
});
```
#### 3. Never Pass Unsanitized Input to exec/execSync
```javascript
// DANGEROUS
const { execSync } = require('child_process');
execSync(`git commit -m "${userMessage}"`); // VULNERABLE!
// BETTER - But still risky
const { execFileSync } = require('child_process');
execFileSync('git', ['commit', '-m', userMessage]);
```
#### 4. Input Validation and Allowlisting
**Use strict allowlisting:**
```javascript
function validateFilename(filename) {
// Only allow alphanumeric, dash, underscore, dot
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
throw new Error('Invalid filename');
}
return filename;
}
const safeFilename = validateFilename(userInput);
execFile('cat', [safeFilename], callback);
```
#### 5. Avoid Shell Invocation
```javascript
// BAD - Shell is spawned
exec('ls -la', callback);
// GOOD - No shell spawned
execFile('ls', ['-la'], callback);
// BAD - Shell option enabled
spawn('ls', ['-la'], { shell: true });
// GOOD - No shell
spawn('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.
```javascript
// STILL VULNERABLE
exec(`cat '${userInput}'`); // Can be bypassed with: file.txt' ; rm -rf / ; echo '
// If you must use shell, consider shell-escape library
const shellescape = require('shell-escape');
const escaped = shellescape([userInput]);
exec(`cat ${escaped}`, callback);
```
### ESLint Security Rules
Enable security linting to catch dangerous patterns:
```javascript
// .eslintrc.js
module.exports = {
plugins: ['security'],
rules: {
'security/detect-child-process': 'error',
'security/detect-non-literal-fs-filename': 'error'
}
};
```
### Build Script Security Checklist
For static site generators and build tools:
- [ ] Never use `exec`/`execSync` with user input or environment variables
- [ ] Prefer `execFile`/`spawn` with 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.json` scripts for command injection risks
- [ ] Never trust data from external sources (API responses, file contents)
- [ ] Use `--ignore-scripts` flag 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
```javascript
// Build script example
import Joi from 'joi';
const configSchema = Joi.object({
outputDir: Joi.string().pattern(/^[a-zA-Z0-9/_-]+$/).required(),
baseUrl: Joi.string().uri().required(),
enableAnalytics: Joi.boolean().default(false)
});
const { error, value: config } = configSchema.validate(process.env);
if (error) {
throw new Error(`Invalid configuration: ${error.message}`);
}
```
#### 2. Allowlisting Over Denylisting
**Allowlisting** (defining exactly what IS authorized) is far superior to denylisting (blocking known-bad patterns).
**Why denylisting fails:**
```javascript
// DANGEROUS - Easily bypassed
function denylistValidation(input) {
if (input.includes("'") || input.includes('
```
**Generate hash:**
```bash
echo -n "console.log('Hello, world!');" | openssl dgst -sha256 -binary | openssl base64
# 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 plugins
- **`base-uri 'none'`** - Prevents base tag injection
- **`require-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)
```html
```
**Limitations of meta tags:**
- No `frame-ancestors` support
- No `sandbox` support
- 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:
```javascript
// build-csp.js
import crypto from 'crypto';
import fs from 'fs/promises';
import { parse } from 'node-html-parser';
async function generateCSP(htmlPath) {
const html = await fs.readFile(htmlPath, 'utf-8');
const root = parse(html);
const scriptHashes = root.querySelectorAll('script')
.filter(script => !script.getAttribute('src')) // Only inline scripts
.map(script => {
const content = script.textContent;
const hash = crypto.createHash('sha256')
.update(content, 'utf-8')
.digest('base64');
return `'sha256-${hash}'`;
});
const csp = `script-src ${scriptHashes.join(' ')} 'strict-dynamic'; object-src 'none'; base-uri 'none'`;
return csp;
}
```
### Subresource Integrity (SRI) for Static Sites
Even fully static sites benefit from CSP for enforcing Subresource Integrity on third-party resources:
```html
```
**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:**
```javascript
// astro.config.mjs
export default {
vite: {
plugins: [
{
name: 'csp-hash-generator',
transformIndexHtml(html) {
// Generate hashes and inject CSP meta tag
}
}
]
}
};
```
**Eleventy example:**
```javascript
// .eleventy.js
module.exports = function(eleventyConfig) {
eleventyConfig.addTransform('csp', async (content, outputPath) => {
if (outputPath.endsWith('.html')) {
// Calculate hashes and inject CSP
}
return content;
});
};
```
### 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:
```javascript
// VULNERABLE static site code
const params = new URLSearchParams(window.location.search);
const username = params.get('name');
document.getElementById('greeting').innerHTML = `Hello, ${username}!`;
// Attack: https://example.com/?name=
```
### Types of XSS in Static Sites
#### 1. DOM-Based XSS
JavaScript directly manipulates the DOM with untrusted data:
```javascript
// VULNERABLE
element.innerHTML = userInput;
document.write(userInput);
eval(userInput);
location.href = userInput;
// SAFE
element.textContent = userInput; // Treats as text, not HTML
element.setAttribute('data-value', userInput);
```
#### 2. Third-Party Script XSS
Compromised CDN or third-party scripts:
```html
```
**Mitigation:** Use Subresource Integrity (SRI) - see Section 6.
#### 3. Client-Side Template Injection
Static site generators using client-side templating:
```javascript
// VULNERABLE - Client-side Markdown/template rendering
const markdown = params.get('content');
const html = markdownToHtml(markdown); // If not sanitized
container.innerHTML = html;
```
### XSS Prevention Best Practices
#### 1. Use Safe DOM APIs
**Prefer safe sinks:**
```javascript
// SAFE APIs
element.textContent = userInput; // Always safe
element.setAttribute('name', value); // Safe for most attributes
element.className = userInput; // Safe
element.value = userInput; // Safe for form inputs
// UNSAFE APIs - Avoid or sanitize first
element.innerHTML = userInput; // DANGEROUS
element.outerHTML = userInput; // DANGEROUS
document.write(userInput); // DANGEROUS
element.insertAdjacentHTML('beforeend', userInput); // DANGEROUS
```
#### 2. Context-Aware Output Encoding
Different contexts require different encoding:
**HTML Context:**
```javascript
function encodeHTML(str) {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Usage
element.innerHTML = `