@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 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.0",
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;
@@ -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
  }
@@ -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
  }
@@ -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
- }