Skip to content

Commit

Permalink
Merge pull request #43 from sambauers/main
Browse files Browse the repository at this point in the history
feat(buildCSPHeaders): allow merging of default directives
  • Loading branch information
trezy committed Dec 4, 2022
2 parents 4ffda6d + e0e1737 commit 18545f0
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 19 deletions.
18 changes: 18 additions & 0 deletions docs/api/contentSecurityPolicy.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ Note that `'self'` is in quotes. This is a CSP thing and `next-safe` does not ha
}
```

### `contentSecurityPolicy.mergeDefaultDirectives`

Setting `contentSecurityPolicy.mergeDefaultDirectives` to `true` will retain the default directive values supplied by `next-safe` and merge them with any additional directives that are added in the configuration. Duplicate values in any directives will be removed. Setting any directive as `false` will disable the directive as usual.

```js
{
contentSecurityPolicy: {
mergeDefaultDirectives: true,
"child-src": false,
// equivalent to - "child-src": false
"img-src": "unsplash.com",
// equivalent to - "img-src": ["'self'", "unsplash.com"]
"font-src": ["fonts.adobe.com", "fonts.gstatic.com"]
// equivalent to - "font-src": ["'self'", "fonts.adobe.com", "fonts.gstatic.com"]
},
}
```

### `contentSecurityPolicy.reportOnly`

Setting `contentSecurityPolicy.reportOnly` to `true` will rename the `Content-Security-Policy` header to `Content-Security-Policy-Report-Only`. This is useful if you want to test your CSP without breaking your site. Make sure to also set up an endpoint to receive the reports, then set your [`contentSecurityPolicy.report-to`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to) field to point to that endpoint.
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ nextSafe({
"script-src": "'self'",
"style-src": "'self'",
"worker-src": "'self'",
mergeDefaultDirectives: false,
reportOnly: false,
},
frameOptions: "DENY",
Expand Down
67 changes: 48 additions & 19 deletions lib/buildCSPHeaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,34 @@ const devDirectives = {
'style-src': ["'unsafe-inline'"],
}

function getCSPDirective(value, defaultValue) {
return [value || defaultValue].flat()
function getCSPDirective(value, defaultValue, mergeDefaultDirectives = false) {
// if user configured value is false, return early
if (value === false) {
return [false]
}

// ensure any string values are split to enable removal of duplicates
const valueArray = [value].flat().reduce((accumulator, current) => {
if (typeof current !== 'string') {
return accumulator
}
accumulator.push(...current.trim().split(/\s+/))
return accumulator
}, [])

// flatten default values
const defaultValueArray = [defaultValue].flat()

// merge values with default if required
const mergedValueArray = mergeDefaultDirectives
? [...defaultValueArray, ...valueArray]
: valueArray

// de-duplicate merged values
const uniqueValueArray = [...new Set(mergedValueArray)]

// only return user configured values if present, otherwise return default
return uniqueValueArray.length > 0 ? uniqueValueArray : defaultValueArray
}

module.exports = function buildCSPHeaders(options = {}) {
Expand All @@ -22,24 +48,27 @@ module.exports = function buildCSPHeaders(options = {}) {
return []
}

// ensure mergeDefaultDirectives option is a boolean, anything other than boolean `true` means `false`
const mergeDefaultDirectives = contentSecurityPolicy.mergeDefaultDirectives === true

// Content Security Policy
const directives = {
'base-uri': getCSPDirective(contentSecurityPolicy['base-uri'], "'none'"),
'child-src': getCSPDirective(contentSecurityPolicy['child-src'], "'none'"),
'connect-src': getCSPDirective(contentSecurityPolicy['connect-src'], "'self'"),
'default-src': getCSPDirective(contentSecurityPolicy['default-src'], "'self'"),
'font-src': getCSPDirective(contentSecurityPolicy['font-src'], "'self'"),
'form-action': getCSPDirective(contentSecurityPolicy['form-action'], "'self'"),
'frame-ancestors': getCSPDirective(contentSecurityPolicy['frame-ancestors'], "'none'"),
'frame-src': getCSPDirective(contentSecurityPolicy['frame-src'], "'none'"),
'img-src': getCSPDirective(contentSecurityPolicy['img-src'], "'self'"),
'manifest-src': getCSPDirective(contentSecurityPolicy['manifest-src'], "'self'"),
'media-src': getCSPDirective(contentSecurityPolicy['media-src'], "'self'"),
'object-src': getCSPDirective(contentSecurityPolicy['object-src'], "'none'"),
'prefetch-src': getCSPDirective(contentSecurityPolicy['prefetch-src'], "'self'"),
'script-src': getCSPDirective(contentSecurityPolicy['script-src'], "'self'"),
'style-src': getCSPDirective(contentSecurityPolicy['style-src'], "'self'"),
'worker-src': getCSPDirective(contentSecurityPolicy['worker-src'], "'self'"),
'base-uri': getCSPDirective(contentSecurityPolicy['base-uri'], "'none'", mergeDefaultDirectives),
'child-src': getCSPDirective(contentSecurityPolicy['child-src'], "'none'", mergeDefaultDirectives),
'connect-src': getCSPDirective(contentSecurityPolicy['connect-src'], "'self'", mergeDefaultDirectives),
'default-src': getCSPDirective(contentSecurityPolicy['default-src'], "'self'", mergeDefaultDirectives),
'font-src': getCSPDirective(contentSecurityPolicy['font-src'], "'self'", mergeDefaultDirectives),
'form-action': getCSPDirective(contentSecurityPolicy['form-action'], "'self'", mergeDefaultDirectives),
'frame-ancestors': getCSPDirective(contentSecurityPolicy['frame-ancestors'], "'none'", mergeDefaultDirectives),
'frame-src': getCSPDirective(contentSecurityPolicy['frame-src'], "'none'", mergeDefaultDirectives),
'img-src': getCSPDirective(contentSecurityPolicy['img-src'], "'self'", mergeDefaultDirectives),
'manifest-src': getCSPDirective(contentSecurityPolicy['manifest-src'], "'self'", mergeDefaultDirectives),
'media-src': getCSPDirective(contentSecurityPolicy['media-src'], "'self'", mergeDefaultDirectives),
'object-src': getCSPDirective(contentSecurityPolicy['object-src'], "'none'", mergeDefaultDirectives),
'prefetch-src': getCSPDirective(contentSecurityPolicy['prefetch-src'], "'self'", mergeDefaultDirectives),
'script-src': getCSPDirective(contentSecurityPolicy['script-src'], "'self'", mergeDefaultDirectives),
'style-src': getCSPDirective(contentSecurityPolicy['style-src'], "'self'", mergeDefaultDirectives),
'worker-src': getCSPDirective(contentSecurityPolicy['worker-src'], "'self'", mergeDefaultDirectives),
}

const optionalDirectives = [
Expand Down Expand Up @@ -78,7 +107,7 @@ module.exports = function buildCSPHeaders(options = {}) {
if (isDev) {
Object.entries(devDirectives).forEach(([key, value]) => {
if (directives[key]) {
directives[key] = directives[key].concat(value)
directives[key] = [...new Set(directives[key].concat(value))]
} else {
directives[key] = [...value]
}
Expand Down
1 change: 1 addition & 0 deletions lib/models/CSP.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@
* @property {CSPDirective} ['upgrade-insecure-requests']
* @property {CSPDirective} ['report-to']
* @property {CSPDirective} ['report-uri']
* @property {boolean} [mergeDefaultDirectives]
* @property {boolean} [reportOnly]
*/

0 comments on commit 18545f0

Please sign in to comment.