From c2f006d989f4e5869f7e5804454d36adf36895fc Mon Sep 17 00:00:00 2001 From: Chris Zuber Date: Tue, 9 Dec 2025 13:21:43 -0800 Subject: [PATCH] Add CSP customization methods and update config Introduces functions to customize Content Security Policy (CSP) sources and policies in csp.js, including methods to add sources and lock CSP arrays. Updates useCSP to handle empty arrays and errors gracefully. Reorders responsePostprocessors in http.config.js and updates package version to 1.0.3 in package.json and package-lock.json. Adds new export mappings in package.json. --- CHANGELOG.md | 5 ++++ csp.js | 71 +++++++++++++++++++++++++++++++++++++++-------- http.config.js | 4 +-- package-lock.json | 4 +-- package.json | 26 ++++++++++++++++- 5 files changed, 93 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d3a31b..da79c77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v1.0.3] - 2025-12-09 + +### Added +- Add methods to customize CSP + ## [v1.0.2] - 2025-12-09 ### Changed diff --git a/csp.js b/csp.js index 16ab4b5..1488f38 100644 --- a/csp.js +++ b/csp.js @@ -4,18 +4,6 @@ export const importmap = new Importmap(); await importmap.importLocalPackage(); export const integrity = await importmap.getIntegrity(); -export function useCSP(policy = { 'default-src': ['\'self\''] }) { - const policyStr = Object.entries(policy).map(([name, values]) => { - return `${name} ${Array.isArray(values) ? values.join(' ') : values}`; - }).join('; '); - - return function (response, { request }) { - if (request.destination === 'document' && ! response.headers.has('Content-Security-Policy')) { - response.headers.set('Content-Security-Policy', policyStr); - } - }; -} - const DEFAULT_SRC = ['\'self\'']; const SCRIPT_SRC = ['\'self\'', 'https://unpkg.com/@shgysk8zer0/', 'https://unpkg.com/@kernvalley/', 'https://unpkg.com/@aegisjsproject/', `'${integrity}'`]; const STYLE_SRC = ['\'self\'', 'https://unpkg.com/@agisjsproject/', 'blob:']; @@ -24,8 +12,63 @@ const MEDIA_SRC = ['\'self\'', 'blob:']; const CONNECT_SRC = ['\'self\'']; const FONT_SRC = ['\'self\'']; const FRAME_SRC = ['\'self\'', 'https://www.youtube-nocookie.com']; +const MANIFEST_SRC = ['\'self\'']; +const PREFETCH_SRC = ['\'self\'']; +const WORKER_SRC = ['\'selfs\'']; +const OBJECT_SRC = []; const TRUSTED_TYPES = ['aegis-sanitizer#html']; +export const lockCSP = () => { + Object.freeze(DEFAULT_SRC); + Object.freeze(SCRIPT_SRC); + Object.freeze(STYLE_SRC); + Object.freeze(IMAGE_SRC); + Object.freeze(MEDIA_SRC); + Object.freeze(CONNECT_SRC); + Object.freeze(FONT_SRC); + Object.freeze(FRAME_SRC); + Object.freeze(MANIFEST_SRC); + Object.freeze(PREFETCH_SRC); + Object.freeze(WORKER_SRC); + Object.freeze(OBJECT_SRC); + Object.freeze(TRUSTED_TYPES); +}; + +export const addDefaultSrc = (...srcs) => DEFAULT_SRC.push(...srcs); +export const addScriptSrc = (...srcs) => SCRIPT_SRC.push(...srcs); +export const addStyleSrc = (...srcs) => STYLE_SRC.push(...srcs); +export const addImageSrc = (...srcs) => IMAGE_SRC.push(...srcs); +export const addMediaSrc = (...srcs) => MEDIA_SRC.push(...srcs); +export const addConnectSrc = (...srcs) => CONNECT_SRC.push(...srcs); +export const addFontSrc = (...srcs) => FONT_SRC.push(...srcs); +export const addFrameSrc = (...srcs) => FRAME_SRC.push(...srcs); +export const addManifestSrc = (...srcs) => MANIFEST_SRC.push(...srcs); +export const addObjectSrc = (...srcs) => OBJECT_SRC.push(...srcs); +export const addPrefetchSrc = (...srcs) => PREFETCH_SRC.push(...srcs); +export const addWorkerSrc = (...srcs) => WORKER_SRC.push(...srcs); +export const addTrustedTypePolicy = (...policies) => TRUSTED_TYPES.push(...policies); + +export function useCSP(policy = { 'default-src': ['\'self\''] }) { + const policyStr = Object.entries(policy).map(([name, values]) => { + return `${name} ${Array.isArray(values) ? values.length === 0 ? '\'none\'' : values.join(' ') : values}`; + }).join('; '); + + /** + * @param {Response} response + * @param {object} config + * @param {Request} [config.request] + */ + return function (response, { request }) { + try { + if (request.destination === 'document' && ! response.headers.has('Content-Security-Policy')) { + response.headers.set('Content-Security-Policy', policyStr); + } + } catch(err) { + console.error(err); + } + }; +} + export const useDefaultCSP = ({ ...rest } = {}) => useCSP({ 'default-src': DEFAULT_SRC, 'script-src': SCRIPT_SRC, @@ -35,6 +78,10 @@ export const useDefaultCSP = ({ ...rest } = {}) => useCSP({ 'font-src': FONT_SRC, 'frame-src': FRAME_SRC, 'connect-src': CONNECT_SRC, + 'manifest-src': MANIFEST_SRC, + 'obejct-src': OBJECT_SRC, + 'prefetch-src': PREFETCH_SRC, + 'worker-src': WORKER_SRC, 'trusted-types': TRUSTED_TYPES, 'require-trusted-types-for': '\'script\'', ...rest diff --git a/http.config.js b/http.config.js index 4a12878..12b0bec 100644 --- a/http.config.js +++ b/http.config.js @@ -23,13 +23,13 @@ export default { ], responsePostprocessors: [ '@aegisjsproject/http-utils/compression.js', - setCacheItem, '@aegisjsproject/http-utils/cors.js', '@aegisjsproject/http-utils/csp.js', (response, { request }) => { if (request.destination === 'document') { response.headers.append('Link', `<${imports['@shgysk8zer0/polyfills']}>; rel="preload"; as="script"; fetchpriority="high"; crossorigin="anonymous"; referrerpolicy="no-referrer"`); } - } + }, + setCacheItem, ], }; diff --git a/package-lock.json b/package-lock.json index 0cfb6e1..4e872b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aegisjsproject/http-utils", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@aegisjsproject/http-utils", - "version": "1.0.2", + "version": "1.0.3", "funding": [ { "type": "librepay", diff --git a/package.json b/package.json index 32eefcc..cd7e7bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aegisjsproject/http-utils", - "version": "1.0.2", + "version": "1.0.3", "description": "HTTP Utilities for @shgysk8zer0/http-server ", "keywords": [], "type": "module", @@ -8,6 +8,30 @@ "./*.js": { "import": "./*.js" }, + "./cache": { + "import": "./cache.js" + }, + "./compression": { + "import": "./compression.js" + }, + "./cors": { + "import": "./cors.js" + }, + "./csp": { + "import": "./csp.js" + }, + "./geo": { + "import": "./geo.js" + }, + "./logger": { + "import": "./logger.js" + }, + "./rate-limit": { + "import": "./rate-limit.js" + }, + "./request-id": { + "import": "./request-id.js" + }, "./*.mjs": { "import": "./*.js" },