Compare commits

...

2 Commits
v1.0.0 ... main

Author SHA1 Message Date
48919236d0 Major rewrite for more defaults 2025-04-15 15:24:28 -06:00
4b35c9fe09 Expanded config for more options 2025-04-15 14:19:01 -06:00
4 changed files with 148 additions and 35 deletions

View File

@ -1,28 +1,72 @@
# Sets # Content Security Policy
# #
# Should yield the follwoiung header: # Should yield the following header:
# "Content-Security-Policy: default-src 'self'; # "Content-Security-Policy: default-src 'self';
# script-src 'self' example.com;object-src 'none'; # script-src 'self' example.com;object-src 'none';
# upgrade-insecure-requests" # upgrade-insecure-requests"
# Note: embedded single quotes are required # Note: embedded single quotes are required
default-src: [ "'self'" ] contentSecurityPolicy:
base-uri: [ "'self'" ] useDefaults: false
font-src: directives:
default-src: ["'self'"] # Allow content only from same origin
base-uri: ["'self'"] # Restrict <base> tag
font-src: # Allow font loading from safe sources
- "'self'" - "'self'"
- "https:" - "https:"
- "data:" - "data:"
form-action: [ "'self'" ] form-action: ["'self'"] # Restrict form submissions
frame-ancestors: [ "'self'" ] frame-ancestors: ["'self'"] # Prevent clickjacking
img-src: img-src: # Allow inline and local images
- "'self'" - "'self'"
- "data:" - "data:"
object-src: [ "'none'" ] object-src: ["'none'"] # Disable <object> usage
script-src: script-src: # Disallow 3rd party scripts by default
- "'self'" - "'self'"
- example.com - example.com
script-src-attr: [ "'none'" ] script-src-attr: ["'none'"] # Disallow inline script attributes
style-src: style-src: # Inline styles okay for frameworks
- "'self'" - "'self'"
- "https:" - "https:"
- "'unsafe-inline'" - "'unsafe-inline'"
upgrade-insecure-requests: [] upgrade-insecure-requests: [] # Auto-upgrade HTTP requests
# Enforce embedding policies
crossOriginEmbedderPolicy:
policy: "require-corp" # Required for shared array buffers
crossOriginOpenerPolicy:
policy: "same-origin" # Isolate window/tab from others
crossOriginResourcePolicy:
policy: "same-origin" # Limit loading of cross-origin resources
# Use origin-based isolation for threads
originAgentCluster: true
# Limit what referrer info is sent
referrerPolicy:
policy: "no-referrer"
# Force HTTPS in browsers
strictTransportSecurity:
maxAge: 15552000 # 180 days
includeSubDomains: true
preload: true
# Don't allow content sniffing
xContentTypeOptions: true
# Disable DNS prefetching
dnsPrefetchControl:
allow: false
# Prevent page from being embedded in <iframe>
frameguard:
action: "SAMEORIGIN"
# Block Flash and Acrobat cross-domain access
permittedCrossDomainPolicies:
permittedPolicies: "none"
# Hide the Express server signature
hidePoweredBy: true

View File

@ -4,14 +4,32 @@ const YAML = require('yaml')
const helmet = require('helmet') const helmet = require('helmet')
module.exports = (path) => { module.exports = (path) => {
const csppolicy = fs.readFileSync(path, 'utf8') let csppolicy
const zero = {
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
crossOriginOpenerPolicy: false,
crossOriginResourcePolicy: false,
originAgentCluster: false,
referrerPolicy: false,
strictTransportSecurity: false,
xContentTypeOptions: false,
dnsPrefetchControl: false,
frameguard: false,
permittedCrossDomainPolicies: false,
hidePoweredBy: false,
};
try {
csppolicy = fs.readFileSync(path, 'utf8')
} catch (e) {
csppolicy = 'contentSecurityPolicy:\n useDefaults: true\n';
}
const csp = YAML.parse(csppolicy) const csp = YAML.parse(csppolicy)
return helmet({ // Mandatory
contentSecurityPolicy: { csp.xXssProtection = false
useDefaults: false, csp.xDownloadOptions = false
directives: csp, csp.expectCt = false
},
xFrameOptions: 'SAMEORIGIN', return helmet({...zero,...csp})
})
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "express-csp", "name": "express-csp",
"version": "1.0.0", "version": "1.1.0",
"description": "Rapid configurable Content Security Policy middleware", "description": "Rapid configurable Content Security Policy middleware",
"main": "./index.js", "main": "./index.js",
"exports": { "exports": {

View File

@ -6,26 +6,40 @@ const fs = require('fs')
// Import the middleware factory (don't name this `csp` to avoid shadowing!) // Import the middleware factory (don't name this `csp` to avoid shadowing!)
const createCspMiddleware = require('../index.cjs') const createCspMiddleware = require('../index.cjs')
describe('Rapid configurable Content Security Policy middleware', () => { describe('Content Security Policy middleware', () => {
const validPolicyPath = path.join(__dirname, '../csp-policy.yml') const validPolicyPath = path.join(__dirname, '../csp-policy.yml')
const malformedPolicyPath = path.join(__dirname, 'bad-policy.yml') const malformedPolicyPath = path.join(__dirname, 'bad-policy.yml')
const missingPolicyPath = path.join(__dirname, 'xxxxxx.yml')
const minimalPolicyPath = path.join(__dirname, 'minimal-policy.yml')
const customPolicyPath = path.join(__dirname, 'custom-policy.yml') const customPolicyPath = path.join(__dirname, 'custom-policy.yml')
beforeAll(() => { beforeAll(() => {
// Write a malformed YAML file (missing colon, bad list syntax) // Write a malformed YAML file (missing colon, bad list syntax)
fs.writeFileSync(malformedPolicyPath, `default-src 'self'\nthis-is: [bad yaml]`) fs.writeFileSync(malformedPolicyPath, `default-src 'self'\nthis-is: [bad yaml]`)
// Write a minimal custom policy
fs.writeFileSync(
minimalPolicyPath,
`
contentSecurityPolicy: false
`,
)
// Write a simple custom policy // Write a simple custom policy
fs.writeFileSync( fs.writeFileSync(
customPolicyPath, customPolicyPath,
` `
default-src: ["'self'"] contentSecurityPolicy:
script-src: ["'self'", "https://cdn.example.com"] directives:
default-src: ["'self'"]
script-src: ["'self'", "https://cdn.example.com"]
`, `,
) )
}) })
afterAll(() => { afterAll(() => {
//fs.unlinkSync(missingPolicyPath)
fs.unlinkSync(minimalPolicyPath)
fs.unlinkSync(malformedPolicyPath) fs.unlinkSync(malformedPolicyPath)
fs.unlinkSync(customPolicyPath) fs.unlinkSync(customPolicyPath)
}) })
@ -37,6 +51,7 @@ script-src: ["'self'", "https://cdn.example.com"]
app.get('/', (req, res) => res.send('Hello World')) app.get('/', (req, res) => res.send('Hello World'))
const res = await request(app).get('/') const res = await request(app).get('/')
console.log(res.headers)
expect(res.headers['content-security-policy']).toBeDefined() expect(res.headers['content-security-policy']).toBeDefined()
expect(res.text).toBe('Hello World') expect(res.text).toBe('Hello World')
}) })
@ -61,6 +76,40 @@ script-src: ["'self'", "https://cdn.example.com"]
}).toThrow(/Implicit keys|bad indentation|unexpected token/i) }).toThrow(/Implicit keys|bad indentation|unexpected token/i)
}) })
it('should show defaults for missing YAML', async () => {
const app = express()
const csp = createCspMiddleware(missingPolicyPath)
app.use(csp)
app.get('/', (req, res) => res.send('Test Custom'))
const res = await request(app).get('/')
res.headers.TEST = 'Missing'
console.log(res.headers)
const cspHeader = res.headers['content-security-policy']
expect(cspHeader).toMatch(/default-src 'self'/)
})
it('should show nothing for minimal YAML', async () => {
const app = express()
const csp = createCspMiddleware(minimalPolicyPath)
app.use(csp)
app.get('/', (req, res) => res.send('Test Custom'))
const res = await request(app).get('/')
res.headers.TEST = 'Minimal'
console.log(res.headers)
const cspHeader = res.headers['content-security-policy']
const allowed = ['x-powered-by', 'content-type', 'content-length']
Object.keys(res.headers).forEach((key) => {
if (!allowed.includes(key.toLowerCase())) {
expect(key).not.toMatch(/^(content|x)-/i)
}
})
})
it('should apply a custom policy file correctly', async () => { it('should apply a custom policy file correctly', async () => {
const app = express() const app = express()
const csp = createCspMiddleware(customPolicyPath) const csp = createCspMiddleware(customPolicyPath)
@ -68,6 +117,8 @@ script-src: ["'self'", "https://cdn.example.com"]
app.get('/', (req, res) => res.send('Test Custom')) app.get('/', (req, res) => res.send('Test Custom'))
const res = await request(app).get('/') const res = await request(app).get('/')
res.headers.TEST = 'Custom'
console.log(res.headers)
const cspHeader = res.headers['content-security-policy'] const cspHeader = res.headers['content-security-policy']
expect(cspHeader).toMatch(/default-src 'self'/) expect(cspHeader).toMatch(/default-src 'self'/)