@adobe/helix-html-pipeline 6.19.0 → 6.20.0

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,17 @@
1
+ # [6.20.0](https://github.com/adobe/helix-html-pipeline/compare/v6.19.1...v6.20.0) (2025-02-12)
2
+
3
+
4
+ ### Features
5
+
6
+ * Enable CSP with nonce for Helix 5 ([#816](https://github.com/adobe/helix-html-pipeline/issues/816)) ([3f4895f](https://github.com/adobe/helix-html-pipeline/commit/3f4895ffc975ea4c35b3ba139bf0ead56a2d0000))
7
+
8
+ ## [6.19.1](https://github.com/adobe/helix-html-pipeline/compare/v6.19.0...v6.19.1) (2025-02-11)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * 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))
14
+
1
15
  # [6.19.0](https://github.com/adobe/helix-html-pipeline/compare/v6.18.4...v6.19.0) (2025-02-11)
2
16
 
3
17
 
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.20.0",
4
4
  "description": "Helix HTML Pipeline",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -57,7 +57,7 @@
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",
60
+ "parse5": "7.2.1",
61
61
  "rehype-format": "5.0.1",
62
62
  "rehype-parse": "9.0.1",
63
63
  "remark-parse": "11.0.0",
package/src/steps/csp.js CHANGED
@@ -9,11 +9,12 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
- import crypto from 'crypto';
13
12
  import { select } from 'hast-util-select';
13
+ import { Tokenizer } from 'parse5';
14
14
  import { remove } from 'unist-util-remove';
15
- import { RewritingStream } from 'parse5-html-rewriting-stream';
16
15
  import { visit } from 'unist-util-visit';
16
+ // eslint-disable-next-line import/no-unresolved
17
+ import cryptoImpl from '#crypto';
17
18
 
18
19
  export const NONCE_AEM = '\'nonce-aem\'';
19
20
 
@@ -58,7 +59,7 @@ function shouldApplyNonce(metaCSPText, headersCSPText) {
58
59
  * @returns {string}
59
60
  */
60
61
  function createNonce() {
61
- return crypto.randomBytes(18).toString('base64');
62
+ return cryptoImpl.randomBytes(18).toString('base64');
62
63
  }
63
64
 
64
65
  /**
@@ -166,47 +167,65 @@ export function contentSecurityPolicyOnCode(state, res) {
166
167
  const nonce = createNonce();
167
168
  let { scriptNonce, styleNonce } = shouldApplyNonce(null, cspHeader);
168
169
 
169
- const rewriter = new RewritingStream();
170
+ const html = res.body;
170
171
  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
172
+ let lastOffset = 0;
173
+
174
+ const getRawHTML = (token) => html.slice(token.location.startOffset, token.location.endOffset);
175
+
176
+ const tokenizer = new Tokenizer({
177
+ sourceCodeLocationInfo: true,
178
+ }, {
179
+ onStartTag(tag) {
180
+ chunks.push(html.slice(lastOffset, tag.location.startOffset));
181
+ try {
182
+ if (tag.tagName === 'meta'
183
+ && tag.attrs.find(
184
+ (attr) => attr.name.toLowerCase() === 'http-equiv' && attr.value.toLowerCase() === 'content-security-policy',
185
+ )
186
+ ) {
187
+ const contentAttr = tag.attrs.find((attr) => attr.name.toLowerCase() === 'content');
188
+ if (contentAttr) {
189
+ ({ scriptNonce, styleNonce } = shouldApplyNonce(contentAttr.value, cspHeader));
190
+
191
+ if (!cspHeader && tag.attrs.find((attr) => attr.name === 'move-as-header' && attr.value === 'true')) {
192
+ res.headers.set('content-security-policy', contentAttr.value.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
193
+ return; // don't push the chunk so it gets removed from the response body
194
+ }
195
+ chunks.push(getRawHTML(tag).replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
196
+ return;
197
+ }
185
198
  }
186
- chunks.push(rawHTML.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));
187
- return;
188
- }
189
- }
190
199
 
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
- }
200
+ if (scriptNonce && tag.tagName === 'script' && tag.attrs.find((attr) => attr.name === 'nonce' && attr.value === 'aem')) {
201
+ chunks.push(getRawHTML(tag).replace(/nonce="aem"/i, `nonce="${nonce}"`));
202
+ return;
203
+ }
195
204
 
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
- }
205
+ if (styleNonce && (tag.tagName === 'style' || tag.tagName === 'link') && tag.attrs.find((attr) => attr.name === 'nonce' && attr.value === 'aem')) {
206
+ chunks.push(getRawHTML(tag).replace(/nonce="aem"/i, `nonce="${nonce}"`));
207
+ return;
208
+ }
200
209
 
201
- chunks.push(rawHTML);
210
+ chunks.push(getRawHTML(tag));
211
+ } finally {
212
+ lastOffset = tag.location.endOffset;
213
+ }
214
+ },
215
+ // no-op callbacks. onStartTag will take care of these
216
+ onComment(_) {},
217
+ onDoctype(_) {},
218
+ onEndTag(_) {},
219
+ onEof(_) {},
220
+ onCharacter(_) {},
221
+ onNullCharacter(_) {},
222
+ onWhitespaceCharacter(_) {},
223
+ onParseError(_) {},
202
224
  });
203
225
 
204
- rewriter.on('data', (data) => {
205
- chunks.push(data);
206
- });
226
+ tokenizer.write(html);
227
+ chunks.push(html.slice(lastOffset));
207
228
 
208
- rewriter.write(res.body);
209
- rewriter.end();
210
229
  res.body = chunks.join('');
211
230
  if (cspHeader) {
212
231
  res.headers.set('content-security-policy', cspHeader.replaceAll(NONCE_AEM, `'nonce-${nonce}'`));