Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48919236d0 | |||
| 4b35c9fe09 |
@ -1,28 +1,72 @@
|
||||
# Sets
|
||||
# Content Security Policy
|
||||
#
|
||||
# Should yield the follwoiung header:
|
||||
# Should yield the following header:
|
||||
# "Content-Security-Policy: default-src 'self';
|
||||
# script-src 'self' example.com;object-src 'none';
|
||||
# upgrade-insecure-requests"
|
||||
# Note: embedded single quotes are required
|
||||
default-src: [ "'self'" ]
|
||||
base-uri: [ "'self'" ]
|
||||
font-src:
|
||||
contentSecurityPolicy:
|
||||
useDefaults: false
|
||||
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'"
|
||||
- "https:"
|
||||
- "data:"
|
||||
form-action: [ "'self'" ]
|
||||
frame-ancestors: [ "'self'" ]
|
||||
img-src:
|
||||
form-action: ["'self'"] # Restrict form submissions
|
||||
frame-ancestors: ["'self'"] # Prevent clickjacking
|
||||
img-src: # Allow inline and local images
|
||||
- "'self'"
|
||||
- "data:"
|
||||
object-src: [ "'none'" ]
|
||||
script-src:
|
||||
object-src: ["'none'"] # Disable <object> usage
|
||||
script-src: # Disallow 3rd party scripts by default
|
||||
- "'self'"
|
||||
- example.com
|
||||
script-src-attr: [ "'none'" ]
|
||||
style-src:
|
||||
script-src-attr: ["'none'"] # Disallow inline script attributes
|
||||
style-src: # Inline styles okay for frameworks
|
||||
- "'self'"
|
||||
- "https:"
|
||||
- "'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
|
||||
|
||||
34
index.cjs
34
index.cjs
@ -4,14 +4,32 @@ const YAML = require('yaml')
|
||||
const helmet = require('helmet')
|
||||
|
||||
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)
|
||||
|
||||
return helmet({
|
||||
contentSecurityPolicy: {
|
||||
useDefaults: false,
|
||||
directives: csp,
|
||||
},
|
||||
xFrameOptions: 'SAMEORIGIN',
|
||||
})
|
||||
// Mandatory
|
||||
csp.xXssProtection = false
|
||||
csp.xDownloadOptions = false
|
||||
csp.expectCt = false
|
||||
|
||||
return helmet({...zero,...csp})
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "express-csp",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Rapid configurable Content Security Policy middleware",
|
||||
"main": "./index.js",
|
||||
"exports": {
|
||||
|
||||
@ -6,19 +6,31 @@ const fs = require('fs')
|
||||
// Import the middleware factory (don't name this `csp` to avoid shadowing!)
|
||||
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 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')
|
||||
|
||||
beforeAll(() => {
|
||||
// Write a malformed YAML file (missing colon, bad list syntax)
|
||||
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
|
||||
fs.writeFileSync(
|
||||
customPolicyPath,
|
||||
`
|
||||
contentSecurityPolicy:
|
||||
directives:
|
||||
default-src: ["'self'"]
|
||||
script-src: ["'self'", "https://cdn.example.com"]
|
||||
`,
|
||||
@ -26,6 +38,8 @@ script-src: ["'self'", "https://cdn.example.com"]
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
//fs.unlinkSync(missingPolicyPath)
|
||||
fs.unlinkSync(minimalPolicyPath)
|
||||
fs.unlinkSync(malformedPolicyPath)
|
||||
fs.unlinkSync(customPolicyPath)
|
||||
})
|
||||
@ -37,6 +51,7 @@ script-src: ["'self'", "https://cdn.example.com"]
|
||||
app.get('/', (req, res) => res.send('Hello World'))
|
||||
|
||||
const res = await request(app).get('/')
|
||||
console.log(res.headers)
|
||||
expect(res.headers['content-security-policy']).toBeDefined()
|
||||
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)
|
||||
})
|
||||
|
||||
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 () => {
|
||||
const app = express()
|
||||
const csp = createCspMiddleware(customPolicyPath)
|
||||
@ -68,6 +117,8 @@ script-src: ["'self'", "https://cdn.example.com"]
|
||||
app.get('/', (req, res) => res.send('Test Custom'))
|
||||
|
||||
const res = await request(app).get('/')
|
||||
res.headers.TEST = 'Custom'
|
||||
console.log(res.headers)
|
||||
const cspHeader = res.headers['content-security-policy']
|
||||
|
||||
expect(cspHeader).toMatch(/default-src 'self'/)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user