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';
|
# "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
|
||||||
|
|||||||
34
index.cjs
34
index.cjs
@ -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})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -6,19 +6,31 @@ 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,
|
||||||
`
|
`
|
||||||
|
contentSecurityPolicy:
|
||||||
|
directives:
|
||||||
default-src: ["'self'"]
|
default-src: ["'self'"]
|
||||||
script-src: ["'self'", "https://cdn.example.com"]
|
script-src: ["'self'", "https://cdn.example.com"]
|
||||||
`,
|
`,
|
||||||
@ -26,6 +38,8 @@ 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'/)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user