UNPKG

seespee

Version:

Create a Content-Security-Policy for a website based on the statically decidable relations

241 lines (227 loc) 6.6 kB
const AssetGraph = require('assetgraph'); const _ = require('lodash'); const urlTools = require('urltools'); module.exports = async function seespee(url, options) { if (url && typeof url === 'object') { options = url; url = options.url; } else { options = options || {}; } if (typeof url !== 'string') { throw new Error('No url given'); } let root = options.root; if (!/^[a-z+-]+:\/\//i.test(url)) { url = urlTools.fsFilePathToFileUrl(url); // If no root is given, assume it's the directory containing the HTML file root = root || url.replace(/\/[^/]+$/, '/'); } const errors = []; const warns = []; const assetGraph = new AssetGraph({ root }); if (options.userAgent) { assetGraph.requestOptions = { headers: { 'User-Agent': options.userAgent, }, }; } assetGraph .on('error', (error) => errors.push(error)) .on('warn', (warn) => warns.push(warn)); await assetGraph.loadAssets(url); await assetGraph.populate({ followRelations: { type: { $nin: [ 'HtmlAnchor', 'SvgAnchor', 'JavaScriptSourceUrl', 'JavaScriptSourceMappingUrl', 'CssSourceUrl', 'CssSourceMappingUrl', 'HtmlPreconnectLink', 'HtmlDnsPrefetchLink', 'HtmlPrefetchLink', 'HtmlPreloadLink', 'HtmlPrerenderLink', 'HtmlResourceHint', 'HtmlSearchLink', 'HtmlAlternateLink', 'JsonUrl', ], }, }, }); await assetGraph.checkIncompatibleTypes(); for (const relation of assetGraph .findRelations({ type: 'HttpRedirect' }) .sort((a, b) => a.id - b.id)) { if (relation.from.isInitial) { relation.to.isInitial = true; relation.from.isInitial = false; } } const origins = _.uniq( assetGraph .findAssets({ type: 'Html', isInitial: true }) .map((asset) => asset.origin) ); if ( origins.length === 1 && /^http/.test(origins[0]) && /^file:/.test(assetGraph.root) ) { assetGraph.root = `${origins[0]}/`; } const initialAssets = assetGraph.findAssets({ isInitial: true }); if (!initialAssets.some((asset) => asset.type === 'Html' && asset.isLoaded)) { throw new Error( `No HTML assets found (${initialAssets .map((asset) => asset.urlOrDescription) .join(' ')})` ); } const originalPolicies = []; for (const htmlAsset of assetGraph.findAssets({ type: 'Html', isInitial: true, isLoaded: true, })) { originalPolicies.push( ...assetGraph .findRelations({ from: htmlAsset, type: 'HtmlContentSecurityPolicy' }) .map((relation) => ({ type: 'meta', name: relation.node.getAttribute('http-equiv'), value: relation.to.text, })) ); if (!options.ignoreExisting) { if (htmlAsset.contentSecurityPolicy) { htmlAsset.addRelation( { type: 'HtmlContentSecurityPolicy', to: { type: 'ContentSecurityPolicy', text: htmlAsset.contentSecurityPolicy, }, }, 'first' ); originalPolicies.push({ type: 'header', name: 'Content-Security-Policy', value: htmlAsset.contentSecurityPolicy, }); } if (htmlAsset.contentSecurityPolicyReportOnly) { const htmlContentSecurityPolicy = htmlAsset.addRelation( { type: 'HtmlContentSecurityPolicy', to: { type: 'ContentSecurityPolicy', text: htmlAsset.contentSecurityPolicyReportOnly, }, }, 'first' ); htmlContentSecurityPolicy.node.setAttribute( 'http-equiv', 'Content-Security-Policy-Report-Only' ); originalPolicies.push({ type: 'header', name: 'Content-Security-Policy-Report-Only', value: htmlAsset.contentSecurityPolicyReportOnly, }); } } let existingHtmlContentSecurityPolicies = assetGraph.findRelations({ from: htmlAsset, type: 'HtmlContentSecurityPolicy', }); if ( options.ignoreExisting && existingHtmlContentSecurityPolicies.length > 0 ) { for (const existingHtmlContentSecurityPolicy of existingHtmlContentSecurityPolicies) { existingHtmlContentSecurityPolicy.detach(); } existingHtmlContentSecurityPolicies = []; } if (existingHtmlContentSecurityPolicies.length === 0) { htmlAsset.addRelation( { type: 'HtmlContentSecurityPolicy', to: { type: 'ContentSecurityPolicy', text: options.include || "default-src 'none'", }, }, 'first' ); } } const infoObject = await assetGraph.reviewContentSecurityPolicy( { type: 'Html', isInitial: true }, { update: true, level: options.level, includePath: options.level >= 2 ? [ 'script-src', 'style-src', 'frame-src', 'object-src', 'manifest-src', 'child-src', ] : false, } ); const initialHtmlAsset = assetGraph.findAssets({ type: 'Html', isInitial: true, })[0]; const htmlContentSecurityPolicies = assetGraph.findRelations({ from: initialHtmlAsset, type: 'HtmlContentSecurityPolicy', }); return { originalPolicies, originalUrl: url, url: initialHtmlAsset.url, contentSecurityPolicy: htmlContentSecurityPolicies .filter( (htmlContentSecurityPolicy) => htmlContentSecurityPolicy.node.getAttribute('http-equiv') === 'Content-Security-Policy' ) .map((htmlContentSecurityPolicy) => htmlContentSecurityPolicy.to.text) .join(', ') || undefined, contentSecurityPolicyReportOnly: htmlContentSecurityPolicies .filter( (htmlContentSecurityPolicy) => htmlContentSecurityPolicy.node.getAttribute('http-equiv') === 'Content-Security-Policy-Report-Only' ) .map((htmlContentSecurityPolicy) => htmlContentSecurityPolicy.to.text) .join(', ') || undefined, warns, errors, assetGraph, policies: htmlContentSecurityPolicies.map((htmlContentSecurityPolicy) => _.defaults( { asset: htmlContentSecurityPolicy.to, }, infoObject[htmlContentSecurityPolicy.to.id] ) ), }; };