@adobe/helix-html-pipeline 6.19.0 → 6.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -2
- package/src/html-pipe.js +1 -2
- package/src/steps/fetch-404.js +0 -2
- package/src/steps/render-code.js +0 -5
- package/src/steps/render.js +0 -2
- package/src/steps/csp.js +0 -214
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [6.19.1](https://github.com/adobe/helix-html-pipeline/compare/v6.19.0...v6.19.1) (2025-02-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Revert "feat: Enable CSP with nonce for Helix 5 ([#773](https://github.com/adobe/helix-html-pipeline/issues/773))" ([#815](https://github.com/adobe/helix-html-pipeline/issues/815)) ([60924f0](https://github.com/adobe/helix-html-pipeline/commit/60924f0cb50961496013be87ef76aaab17770b79))
|
|
7
|
+
|
|
1
8
|
# [6.19.0](https://github.com/adobe/helix-html-pipeline/compare/v6.18.4...v6.19.0) (2025-02-11)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-html-pipeline",
|
|
3
|
-
"version": "6.19.
|
|
3
|
+
"version": "6.19.1",
|
|
4
4
|
"description": "Helix HTML Pipeline",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -57,7 +57,6 @@
|
|
|
57
57
|
"mdast-util-to-string": "4.0.0",
|
|
58
58
|
"micromark-util-subtokenize": "2.0.4",
|
|
59
59
|
"mime": "4.0.6",
|
|
60
|
-
"parse5-html-rewriting-stream": "7.0.0",
|
|
61
60
|
"rehype-format": "5.0.1",
|
|
62
61
|
"rehype-parse": "9.0.1",
|
|
63
62
|
"remark-parse": "11.0.0",
|
package/src/html-pipe.js
CHANGED
|
@@ -149,7 +149,6 @@ export async function htmlPipe(state, req) {
|
|
|
149
149
|
|
|
150
150
|
if (state.content.sourceBus === 'code' || state.info.originalExtension === '.md') {
|
|
151
151
|
state.timer?.update('serialize');
|
|
152
|
-
await setCustomResponseHeaders(state, req, res);
|
|
153
152
|
await renderCode(state, req, res);
|
|
154
153
|
} else {
|
|
155
154
|
state.timer?.update('parse');
|
|
@@ -166,7 +165,6 @@ export async function htmlPipe(state, req) {
|
|
|
166
165
|
await createPictures(state);
|
|
167
166
|
await extractMetaData(state, req);
|
|
168
167
|
await addHeadingIds(state);
|
|
169
|
-
await setCustomResponseHeaders(state, req, res);
|
|
170
168
|
await render(state, req, res);
|
|
171
169
|
state.timer?.update('serialize');
|
|
172
170
|
await tohtml(state, req, res);
|
|
@@ -174,6 +172,7 @@ export async function htmlPipe(state, req) {
|
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
setLastModified(state, res);
|
|
175
|
+
await setCustomResponseHeaders(state, req, res);
|
|
177
176
|
await setXSurrogateKeyHeader(state, req, res);
|
|
178
177
|
} catch (e) {
|
|
179
178
|
res.error = e.message;
|
package/src/steps/fetch-404.js
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
import { extractLastModified, recordLastModified } from '../utils/last-modified.js';
|
|
13
|
-
import { contentSecurityPolicyOnCode } from './csp.js';
|
|
14
13
|
import { computeContentPathKey, computeCodePathKey } from './set-x-surrogate-key-header.js';
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -35,7 +34,6 @@ export default async function fetch404(state, req, res) {
|
|
|
35
34
|
|
|
36
35
|
// keep 404 response status
|
|
37
36
|
res.body = ret.body;
|
|
38
|
-
contentSecurityPolicyOnCode(state, res);
|
|
39
37
|
res.headers.set('last-modified', ret.headers.get('last-modified'));
|
|
40
38
|
res.headers.set('content-type', 'text/html; charset=utf-8');
|
|
41
39
|
}
|
package/src/steps/render-code.js
CHANGED
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
import mime from 'mime';
|
|
13
|
-
import {
|
|
14
|
-
contentSecurityPolicyOnCode,
|
|
15
|
-
} from './csp.js';
|
|
16
13
|
|
|
17
14
|
const CHARSET_RE = /charset=([^()<>@,;:"/[\]?.=\s]*)/i;
|
|
18
15
|
|
|
@@ -35,6 +32,4 @@ export default async function renderCode(state, req, res) {
|
|
|
35
32
|
}
|
|
36
33
|
}
|
|
37
34
|
res.headers.set('content-type', contentType);
|
|
38
|
-
|
|
39
|
-
contentSecurityPolicyOnCode(state, res);
|
|
40
35
|
}
|
package/src/steps/render.js
CHANGED
|
@@ -15,7 +15,6 @@ import { h } from 'hastscript';
|
|
|
15
15
|
import { unified } from 'unified';
|
|
16
16
|
import rehypeParse from 'rehype-parse';
|
|
17
17
|
import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
|
|
18
|
-
import { contentSecurityPolicyOnAST } from './csp.js';
|
|
19
18
|
|
|
20
19
|
function appendElement($parent, $el) {
|
|
21
20
|
if ($el) {
|
|
@@ -103,7 +102,6 @@ export default async function render(state, req, res) {
|
|
|
103
102
|
const $headHtml = await unified()
|
|
104
103
|
.use(rehypeParse, { fragment: true })
|
|
105
104
|
.parse(headHtml);
|
|
106
|
-
contentSecurityPolicyOnAST(res, $headHtml);
|
|
107
105
|
$head.children.push(...$headHtml.children);
|
|
108
106
|
}
|
|
109
107
|
|
package/src/steps/csp.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2024 Adobe. All rights reserved.
|
|
3
|
-
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
-
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
-
*
|
|
7
|
-
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
-
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
-
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
-
* governing permissions and limitations under the License.
|
|
11
|
-
*/
|
|
12
|
-
import crypto from 'crypto';
|
|
13
|
-
import { select } from 'hast-util-select';
|
|
14
|
-
import { remove } from 'unist-util-remove';
|
|
15
|
-
import { RewritingStream } from 'parse5-html-rewriting-stream';
|
|
16
|
-
import { visit } from 'unist-util-visit';
|
|
17
|
-
|
|
18
|
-
export const NONCE_AEM = '\'nonce-aem\'';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Parse a CSP string into its directives
|
|
22
|
-
* @param {string | undefined | null} csp
|
|
23
|
-
* @returns {Object}
|
|
24
|
-
*/
|
|
25
|
-
function parseCSP(csp) {
|
|
26
|
-
if (!csp) {
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const parts = csp.split(';');
|
|
31
|
-
const result = {};
|
|
32
|
-
parts.forEach((part) => {
|
|
33
|
-
const [directive, ...values] = part.trim().split(' ');
|
|
34
|
-
result[directive] = values.join(' ');
|
|
35
|
-
});
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Computes where nonces should be applied
|
|
41
|
-
* @param {string | null | undefined} metaCSPText The actual CSP value from the meta tag
|
|
42
|
-
* @param {string | null | undefined} headersCSPText The actual CSP value from the headers
|
|
43
|
-
* @returns {scriptNonce: boolean, styleNonce: boolean}
|
|
44
|
-
*/
|
|
45
|
-
function shouldApplyNonce(metaCSPText, headersCSPText) {
|
|
46
|
-
const metaBased = parseCSP(metaCSPText);
|
|
47
|
-
const headersBased = parseCSP(headersCSPText);
|
|
48
|
-
return {
|
|
49
|
-
scriptNonce: metaBased['script-src']?.includes(NONCE_AEM)
|
|
50
|
-
|| headersBased['script-src']?.includes(NONCE_AEM),
|
|
51
|
-
styleNonce: metaBased['style-src']?.includes(NONCE_AEM)
|
|
52
|
-
|| headersBased['style-src']?.includes(NONCE_AEM),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Create a nonce for CSP
|
|
58
|
-
* @returns {string}
|
|
59
|
-
*/
|
|
60
|
-
function createNonce() {
|
|
61
|
-
return crypto.randomBytes(18).toString('base64');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Get the applied CSP header from a response
|
|
66
|
-
* @param {PipelineResponse} res
|
|
67
|
-
* @returns {string}
|
|
68
|
-
*/
|
|
69
|
-
export function getHeaderCSP(res) {
|
|
70
|
-
return res.headers?.get('content-security-policy');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Apply CSP with nonces on an AST
|
|
75
|
-
* @param {PipelineResponse} res
|
|
76
|
-
* @param {Object} tree
|
|
77
|
-
* @param {Object} metaCSP
|
|
78
|
-
* @param {string} headersCSP
|
|
79
|
-
*/
|
|
80
|
-
function createAndApplyNonceOnAST(res, tree, metaCSP, headersCSP) {
|
|
81
|
-
const nonce = createNonce();
|
|
82
|
-
const { scriptNonce, styleNonce } = shouldApplyNonce(metaCSP?.properties.content, headersCSP);
|
|
83
|
-
|
|
84
|
-
if (metaCSP) {
|
|
85
|
-
metaCSP.properties.content = metaCSP.properties.content.replaceAll(NONCE_AEM, `'nonce-${nonce}'`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (headersCSP) {
|
|
89
|
-
res.headers.set('content-security-policy', headersCSP.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
visit(tree, (node) => {
|
|
93
|
-
if (scriptNonce && node.tagName === 'script' && node.properties?.nonce === 'aem') {
|
|
94
|
-
node.properties.nonce = nonce;
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (styleNonce
|
|
99
|
-
&& (node.tagName === 'style' || (node.tagName === 'link' && node.properties?.rel?.[0] === 'stylesheet'))
|
|
100
|
-
&& node.properties?.nonce === 'aem'
|
|
101
|
-
) {
|
|
102
|
-
node.properties.nonce = nonce;
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function checkResponseBodyForMetaBasedCSP(res) {
|
|
108
|
-
return res.body?.includes('http-equiv="content-security-policy"')
|
|
109
|
-
|| res.body?.includes('http-equiv="Content-Security-Policy"');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function checkResponseBodyForAEMNonce(res) {
|
|
113
|
-
/*
|
|
114
|
-
we only look for 'nonce-aem' (single quote) to see if there is a meta CSP with nonce
|
|
115
|
-
we don't want to generate nonces if they appear just on script/style tags,
|
|
116
|
-
as those have no effect without the actual CSP meta (or header).
|
|
117
|
-
this means it is ok to not check for the "nonce-aem" (double quotes)
|
|
118
|
-
*/
|
|
119
|
-
return res.body?.includes(NONCE_AEM);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function getMetaCSP(tree) {
|
|
123
|
-
return select('meta[http-equiv="content-security-policy"]', tree)
|
|
124
|
-
|| select('meta[http-equiv="Content-Security-Policy"]', tree);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function contentSecurityPolicyOnAST(res, tree) {
|
|
128
|
-
const metaCSP = getMetaCSP(tree);
|
|
129
|
-
const headersCSP = getHeaderCSP(res);
|
|
130
|
-
|
|
131
|
-
if (!metaCSP && !headersCSP) {
|
|
132
|
-
// No CSP defined
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// CSP with nonce
|
|
137
|
-
if (metaCSP?.properties.content.includes(NONCE_AEM) || headersCSP?.includes(NONCE_AEM)) {
|
|
138
|
-
createAndApplyNonceOnAST(res, tree, metaCSP, headersCSP);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (metaCSP?.properties['move-as-header'] === 'true') {
|
|
142
|
-
if (!headersCSP) {
|
|
143
|
-
// if we have a CSP in meta but no CSP in headers
|
|
144
|
-
// we can move the CSP from meta to headers, if requested
|
|
145
|
-
res.headers.set('content-security-policy', metaCSP.properties.content);
|
|
146
|
-
remove(tree, null, metaCSP);
|
|
147
|
-
} else {
|
|
148
|
-
delete metaCSP.properties['move-as-header'];
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function contentSecurityPolicyOnCode(state, res) {
|
|
154
|
-
if (state.type !== 'html') {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const cspHeader = getHeaderCSP(res);
|
|
159
|
-
if (!(
|
|
160
|
-
cspHeader?.includes(NONCE_AEM)
|
|
161
|
-
|| (checkResponseBodyForMetaBasedCSP(res) && checkResponseBodyForAEMNonce(res))
|
|
162
|
-
)) {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const nonce = createNonce();
|
|
167
|
-
let { scriptNonce, styleNonce } = shouldApplyNonce(null, cspHeader);
|
|
168
|
-
|
|
169
|
-
const rewriter = new RewritingStream();
|
|
170
|
-
const chunks = [];
|
|
171
|
-
|
|
172
|
-
rewriter.on('startTag', (tag, rawHTML) => {
|
|
173
|
-
if (tag.tagName === 'meta'
|
|
174
|
-
&& tag.attrs.find(
|
|
175
|
-
(attr) => attr.name.toLowerCase() === 'http-equiv' && attr.value.toLowerCase() === 'content-security-policy',
|
|
176
|
-
)
|
|
177
|
-
) {
|
|
178
|
-
const contentAttr = tag.attrs.find((attr) => attr.name.toLowerCase() === 'content');
|
|
179
|
-
if (contentAttr) {
|
|
180
|
-
({ scriptNonce, styleNonce } = shouldApplyNonce(contentAttr.value, cspHeader));
|
|
181
|
-
|
|
182
|
-
if (!cspHeader && tag.attrs.find((attr) => attr.name === 'move-as-header' && attr.value === 'true')) {
|
|
183
|
-
res.headers.set('content-security-policy', contentAttr.value.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
|
|
184
|
-
return; // don't push the chunk so it gets removed from the response body
|
|
185
|
-
}
|
|
186
|
-
chunks.push(rawHTML.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (scriptNonce && tag.tagName === 'script' && tag.attrs.find((attr) => attr.name === 'nonce' && attr.value === 'aem')) {
|
|
192
|
-
chunks.push(rawHTML.replace(/nonce="aem"/i, `nonce="${nonce}"`));
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (styleNonce && (tag.tagName === 'style' || tag.tagName === 'link') && tag.attrs.find((attr) => attr.name === 'nonce' && attr.value === 'aem')) {
|
|
197
|
-
chunks.push(rawHTML.replace(/nonce="aem"/i, `nonce="${nonce}"`));
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
chunks.push(rawHTML);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
rewriter.on('data', (data) => {
|
|
205
|
-
chunks.push(data);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
rewriter.write(res.body);
|
|
209
|
-
rewriter.end();
|
|
210
|
-
res.body = chunks.join('');
|
|
211
|
-
if (cspHeader) {
|
|
212
|
-
res.headers.set('content-security-policy', cspHeader.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
|
|
213
|
-
}
|
|
214
|
-
}
|