Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for cspNonce #2755

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/lucky-panthers-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@hashicorp/design-system-components": minor
---

`hds-code-editor` modifier - Add `cspNonce` argument and automatice nonce detection

`CodeEditor` - Add `cspNonce` argument and automatice nonce detection
37 changes: 35 additions & 2 deletions packages/components/src/modifiers/hds-code-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface HdsCodeEditorSignature {
ariaDescribedBy?: string;
ariaLabel?: string;
ariaLabelledBy?: string;
cspNonce?: string;
hasLineWrapping?: boolean;
language?: HdsCodeEditorLanguages;
value?: string;
Expand All @@ -55,6 +56,27 @@ async function defineStreamLanguage(streamParser: StreamParserType<unknown>) {
return StreamLanguage.define(streamParser);
}

export function getCSPNonceFromMeta(): string | undefined {
const meta = document.querySelector(
'meta[http-equiv="Content-Security-Policy"]'
);

if (meta === null) {
return undefined;
}

const content = meta.getAttribute('content');

if (content === null) {
return undefined;
}

// searches for either "style-src" or "script-src" followed by anything until a token like 'nonce-<value>'
const match = content.match(/(?:style-src|script-src)[^;]*'nonce-([^']+)'/);

return match ? match[1] : undefined;
}

const LOADER_HEIGHT = '164px';

const LANGUAGES: Record<
Expand Down Expand Up @@ -276,7 +298,7 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu

private _buildExtensionsTask = task(
{ drop: true },
async ({ language, hasLineWrapping }) => {
async ({ cspNonce, language, hasLineWrapping }) => {
const [
{
keymap,
Expand Down Expand Up @@ -337,6 +359,13 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
extensions = [languageExtension, ...extensions];
}

// add nonce to the editor view if it exists
const nonce = cspNonce ?? getCSPNonceFromMeta();

if (nonce !== undefined) {
extensions = [...extensions, EditorView.cspNonce.of(nonce)];
}

return extensions;
}
);
Expand All @@ -346,18 +375,20 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
async (
element: HTMLElement,
{
cspNonce,
language,
value,
hasLineWrapping,
}: Pick<
HdsCodeEditorSignature['Args']['Named'],
'language' | 'value' | 'hasLineWrapping'
'cspNonce' | 'language' | 'value' | 'hasLineWrapping'
>
) => {
try {
const { EditorState } = await import('@codemirror/state');

const extensions = await this._buildExtensionsTask.perform({
cspNonce,
language,
hasLineWrapping: hasLineWrapping ?? false,
});
Expand Down Expand Up @@ -395,6 +426,7 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
ariaDescribedBy,
ariaLabel,
ariaLabelledBy,
cspNonce,
hasLineWrapping,
language,
value,
Expand All @@ -406,6 +438,7 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
this.element = element;

const editor = await this._createEditorTask.perform(element, {
cspNonce,
language,
value,
hasLineWrapping,
Expand Down
99 changes: 99 additions & 0 deletions showcase/tests/unit/modifiers/hds-code-editor-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { module, test } from 'qunit';

import { getCSPNonceFromMeta } from '@hashicorp/design-system-components/modifiers/hds-code-editor';

module('Unit | Helper | hds-code-editor', function (hooks) {
hooks.afterEach(() => {
const meta = document.querySelector(
'meta[http-equiv="Content-Security-Policy"]'
);

if (meta) {
meta.parentNode.removeChild(meta);
}
});

test('returns undefined when no meta tag is present', function (assert) {
const existing = document.querySelector(
'meta[http-equiv="Content-Security-Policy"]'
);

if (existing) {
existing.parentNode.removeChild(existing);
}

assert.strictEqual(
getCSPNonceFromMeta(),
undefined,
'Should return undefined if no meta tag is found'
);
});

test('returns undefined when meta tag is present without a content attribute', function (assert) {
const meta = document.createElement('meta');

meta.setAttribute('http-equiv', 'Content-Security-Policy');

document.head.appendChild(meta);

assert.strictEqual(
getCSPNonceFromMeta(),
undefined,
'Should return undefined if content attribute is missing'
);
});

test('extracts nonce from a meta tag with a style-src directive', function (assert) {
const meta = document.createElement('meta');

meta.setAttribute('http-equiv', 'Content-Security-Policy');
meta.setAttribute(
'content',
"default-src 'none'; style-src 'nonce-ABC123';"
);

document.head.appendChild(meta);

assert.strictEqual(
getCSPNonceFromMeta(),
'ABC123',
'Should extract nonce "ABC123" from style-src directive'
);
});

test('extracts nonce from a meta tag with a script-src directive', function (assert) {
const meta = document.createElement('meta');

meta.setAttribute('http-equiv', 'Content-Security-Policy');
meta.setAttribute(
'content',
"default-src 'none'; script-src 'nonce-XYZ789';"
);

document.head.appendChild(meta);

assert.strictEqual(
getCSPNonceFromMeta(),
'XYZ789',
'Should extract nonce "XYZ789" from script-src directive'
);
});

test('returns undefined if nonce is not present in the meta content', function (assert) {
const meta = document.createElement('meta');

meta.setAttribute('http-equiv', 'Content-Security-Policy');
meta.setAttribute(
'content',
"default-src 'none'; style-src 'unsafe-inline';"
);

document.head.appendChild(meta);

assert.strictEqual(
getCSPNonceFromMeta(),
undefined,
'Should return undefined if nonce is not present'
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ This component uses [CodeMirror 6](https://codemirror.net/) under the hood.
<C.Property @name="copyButtonText" @type="string" @default="'Copy'">
Override this value to provide a meaningful `aria-label` for the [`Copy::Button`](/components/copy/button) component.
</C.Property>
<C.Property @name="cspNonce" @type="string">
Provides a Content Security Policy nonce to use when creating the style sheets for the editor. If none is provided, the editor will attempt to extract a nonce from the Content Security Policy.
</C.Property>
<C.Property @name="hasFullScreenButton" @type="boolean" @default="false">
Used to control whether a toggle button for toggling full-screen mode will be displayed.
</C.Property>
Expand Down
Loading