csp #

Fuz supports SvelteKit's config for Content Security Policies with the create_csp_directives helper. Fuz also provides related helpers, types, and CSP data.

The goal is to provide a simple trust modeling system that balances safety+security+privacy with ergonomics, helping users maintain secure policies without unhelpful burden or restriction. It's restrictive by default and easy to set granular overrides, and there's tiered grants for convenience.

Example usage:

import {create_csp_directives, type Csp_Source_Spec} from '@ryanatkn/fuz/csp.js';

// Create the default CSP with no trusted sources except 'self' and some sensible fallbacks.
// This tries to balance security and privacy with usability,
// helping nonexperts write secure policies while still supporting advanced users.
// More later on the details of the defaults.
const csp = create_csp_directives();
// Use in svelte.config.js:
// export default {kit: {csp}}

// Create a CSP with some trusted sources, using Fuz's CSP default trust levels:
export const my_csp_trusted_sources: Array<Csp_Source_Spec> = [
	// Trust in yourself:
	{source: 'https://my.domain/', trust: 'high'},
	// No scripting allowed on these subdomains:
	{source: 'https://*.my.domain/', trust: 'medium'}, 
	// Low but allow scripting:
	{source: 'https://me.github.io/', trust: 'low', directives: ['script-src-elem']},
];
const csp = create_csp_directives({
  trusted_sources: my_csp_trusted_sources,
});

// Create a CSP that opts out of using Fuz's trust abstraction:
create_csp_directives({
	directives: {
		'img-src': ['self', 'https://*.my.domain/'],
		// ...your explicit directives
	},
	// Simply omit `trusted_sources`,
	// but note the above directives extend the base defaults.
});

// Create a CSP with no hidden base defaults,
// so it's fully declarative and explicit,
// like not using Fuz's CSP helpers at all:
const precise_csp = create_csp_directives({
	value_defaults_base: null,
	required_trust_defaults_base: null,
	value_defaults: {
		'img-src': ['self', 'https://my.domain/'],
		'connect-src': ['self', 'https://my.domain/'],
	},
});
// assert.deepEqual(precise_csp, {
// 	'img-src': ['self', 'https://my.domain/'],
// 	'connect-src': ['self', 'https://my.domain/'],
// });

// Transform/extend directives by passing a function:
const custom_csp = create_csp_directives({
  trusted_sources: my_csp_trusted_sources,
  directives: {
    // Add additional domains to existing values:
    'img-src': (v) => [...v, 'trusted.domain'], // extend trusted sources

     // Or completely replace values:
    'connect-src': ['self', 'trusted.domain'], // no base trusted sources!
    'connect-src': () => ['self', 'trusted.domain'], // equivalent

    // Example opt-in to eval:
    'script-src-elem': (v) => [...v, 'unsafe-eval', 'wasm-unsafe-eval'], // alert alert

		// Returning `undefined` or `null` removes the directive,
		// all other values are passed through to SvelteKit.
  },
});

Auditability and transparency are key concerns for the API, but some features are designed to help you to trade away some directness for ergonomics, with the idea that we make it easy for nonexpert users to safely configure basic scenarios, and advanced users can opt into using the API with full declarative transparency (and more verbosity and information load).

Fuz defines an optional system with three levels of trust/risk/sensitivity (low/medium/high, Csp_Trust_Level) that can be configured for each trusted source to give blanket permissions at a specified tier. Granular overrides are straightforward and declarative.

I'm trying to design for full customizability with clear, intuitive boundaries with escalating security and privacy implications. Fuz includes a debatable set of defaults, and input is appreciated to help tune the tradeoffs.

Trust
#

Fuz provides an optional CSP abstraction with three trust levels (of type Csp_Trust_Level) with tiers of escalating risk and implied permission. Sources can opt-in to blanket permissions at a specific level:

export const my_csp_trusted_sources: Array<Csp_Source_Spec> = [
	{source: 'https://a.domain/'}, // undefined `trust` - same as null
	{source: 'https://b.domain/', trust: null}, // no trust
	{source: 'https://c.domain/', trust: 'low'}, // passive resources only
	{source: 'https://d.domain/', trust: 'medium'}, // no script execution
	{source: 'https://e.domain/', trust: 'high'}, // arbitrary code execution
];
trust levelwhat it meansconfigured by required_trust_defaults_base
null
No trust - used for directives that don't support sources'default-src', 'script-src-attr', 'manifest-src', 'child-src', 'object-src', 'base-uri', 'upgrade-insecure-requests', 'report-to', 'require-trusted-types-for', 'trusted-types', 'sandbox'
'low'
Passive resources only - no script execution, no styling or UI control'img-src', 'media-src', 'font-src'
'medium'
Content that may affect layout, styling, or embed external browsing contexts, but cannot directly run code in the page's JS execution environment'style-src', 'style-src-elem', 'style-src-attr', 'connect-src', 'frame-src', 'frame-ancestors', 'form-action', 'worker-src'
'high'
Sources that can execute arbitrary code in the page's context'script-src', 'script-src-elem'

The trust system introduces opt-in abstraction and indirection, and a downside of the design is that it encourages over-permissioning at each individual tier. The maintainers currently feel that this granularity with 3 tiers offers an intuitive base that gets most of the important questions right most of the time for most users, and additional safeguards are available for those that want tighter control or less chance of error.

Explicit directives
#

The CSP helpers have a convenient, declarative API for defining directives per source. These override any defaults, and unlike trust, the directives do not depend on an abstraction layer, so WYSIWYG.

export const my_csp_trusted_sources: Array<Csp_Source_Spec> = [
	{source: 'https://a.domain/'}, // No explicit directives, will use trust level if any
	{source: 'https://b.domain/', directives: null}, // Explicitly no directives
	{source: 'https://c.domain/', directives: ['img-src']}, // Only use for images
	{source: 'https://d.domain/', directives: ['connect-src', 'font-src']}, // Allow for connections and fonts
];

Explicit directives are additive with the trust system. For example, a source with trust: 'low' would normally not be allowed for connect-src, but you can explicitly permit this by including connect-src in the directives array.

// Example: explicitly allowing a source for specific directives regardless of trust
export const my_csp_trusted_sources: Array<Csp_Source_Spec> = [
	// Allow for specific directives (adds to what trust level allows):
	{source: 'https://a.domain/', trust: 'low', directives: ['connect-src']},
	
	// Trust-level provides baseline permissions, explicit directives add specific ones:
	{source: 'https://b.domain/', trust: 'medium', directives: ['script-src-elem']},
	
	// Both mechanisms work together - trust level provides baseline permissions
	// and explicit directives add specific permissions
];

Base defaults
#

The options value_defaults_base (defaults to csp_directive_value_defaults) and required_trust_defaults_base (defaults to csp_directive_required_trust_defaults) afford full control over defaults:

// Start with completely empty defaults (fully declarative):
const minimal_csp = create_csp_directives({
	// Set both base values to null or {} to reset defaults
	value_defaults_base: null, // or {} for same effect
	required_trust_defaults_base: null, // or {} for same effect
	
	// Define only what you need
	value_defaults: {
		'script-src': ['self'],
		'img-src': ['self', 'data:'],
	},
});
// The above is equivalent to not using Fuz's CSP abstraction at all:
assert.deepEqual(minimal_csp, {
	'script-src': ['self'],
	'img-src': ['self', 'data:'],
});

// Use your own custom base value defaults:
create_csp_directives({
	// Define your own value defaults base
	value_defaults_base: {
		'default-src': ['none'],
		'script-src': ['self'],
		'img-src': ['self', 'data:'],
	},
	
	// Override specific directives in the base
	value_defaults: {
		'script-src': ['self', 'https://trusted.domain/'],
	}
});

// Set custom trust requirements for directives:
create_csp_directives({
	// Define your own trust requirements base
	required_trust_defaults_base: {
		'script-src': 'high',
		'connect-src': 'medium',
		'img-src': 'low',
	},
	
	// Source will be added based on your custom trust requirements
	trusted_sources: [
		// This source gets trusted for script-src and connect-src and no other directives.
		// If the `required_trust_defaults_base` were omitted, it would have the normal defaults.
		{source: 'https://somewhat.trusted.domain/', trust: 'medium'},
	]
});

Directive specs
#

The exported csp_directive_specs has JSON data about the CSP directives. Fuz omits deprecated directives.

directivefallbackfallback of
default-srcscript-src, script-src-elem, script-src-attr, style-src, style-src-elem, style-src-attr, img-src, media-src, font-src, manifest-src, child-src, connect-src, worker-src, object-src
script-srcdefault-srcscript-src-elem, script-src-attr, worker-src
script-src-elemscript-src, default-src
script-src-attrscript-src, default-src
style-srcdefault-srcstyle-src-elem, style-src-attr
style-src-elemstyle-src, default-src
style-src-attrstyle-src, default-src
img-srcdefault-src
media-srcdefault-src
font-srcdefault-src
manifest-srcdefault-src
child-srcdefault-srcframe-src, worker-src
connect-srcdefault-src
frame-srcchild-src
frame-ancestors
form-action
worker-srcchild-src, script-src, default-src
object-srcdefault-src
base-uri
upgrade-insecure-requests
report-to
require-trusted-types-for
trusted-types
sandbox