Skip to content

Commit caee01b

Browse files
Add nonce instead of unsafe-inline for naive-ui (#29)
* Add nonce instead of unsafe-inline * Support getting stacktrace for Safari * Add timeout for e2e tests
1 parent 557cf51 commit caee01b

File tree

4 files changed

+101
-2
lines changed

4 files changed

+101
-2
lines changed

e2e/vue.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ test("check menu items", async ({ page }) => {
1212
const $menu = page.locator('[data-test-id="page-header-menu"]');
1313
const menuItems = $menu.getByRole("menuitem");
1414
const $$menuItems = await menuItems.all();
15+
await page.waitForTimeout(500);
1516
await expect(menuItems).toHaveCount(4);
1617

1718
await expect($$menuItems[0]).toHaveText("Home");
@@ -26,6 +27,7 @@ test("General flow", async ({ page, context, browserName }) => {
2627
}
2728

2829
await page.goto("/");
30+
await page.waitForTimeout(500);
2931
await page.getByRole("link", { name: "Create Crypto Address" }).click();
3032
await page.waitForURL("/create-wallets/", { timeout: 1000 });
3133

src/App.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@
77
import { PageHeader } from "@/entities/PageHeader";
88
import { ParanoidMode } from "@/entities/ParanoidMode";
99
import ThemeSwitcher from "@/entities/ThemeSwitcher/ThemeSwitcher.vue";
10+
import { addNonceToStyles } from "@/shared/lib/csp/addNonceToStyles";
1011
import initTracker from "@/shared/lib/tracker/initTracker";
1112
import PageTemplate from "@/shared/ui/PageTemplate/PageTemplate.vue";
1213
14+
// [tag-nonce]
15+
// Some libs doesn't support nonce, part of logic with workaround
16+
// Search by tag in the code
17+
const chunksWithoutNonce = ["naive-ui"];
18+
addNonceToStyles(import.meta.env.VITE_NONCE, chunksWithoutNonce);
19+
1320
const isDark = useDark() as Ref<boolean>;
1421
const themeProvider = computed(() => (isDark.value ? darkTheme : null));
1522
const toggleDark = useToggle(isDark);
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
const originalAppendChild = Node.prototype.appendChild;
2+
const originalInsertBefore = Node.prototype.insertBefore;
3+
4+
const assetsFolder =
5+
import.meta.env.MODE === "production"
6+
? "/assets/"
7+
: "/node_modules/.vite/deps/";
8+
9+
/**
10+
* Check if the current chunk is one of those that do not support nonce
11+
*
12+
* [tag-nonce] Search by tag in the code
13+
*/
14+
function isSelectedChunk(chunksWithoutNonce: string[]) {
15+
const { stack } = new Error();
16+
const pathOfChunks = chunksWithoutNonce.map(
17+
(chunk) => window.location.origin + assetsFolder + chunk,
18+
);
19+
20+
return !!stack
21+
?.split("\n")
22+
.map((line) => {
23+
const chromeMatch = line.match(/at.*\((http.+?)\)$/);
24+
if (chromeMatch) {
25+
return chromeMatch[1];
26+
}
27+
28+
const safariMatch = line.match(/@(http.+?):\d+:\d+/);
29+
if (safariMatch) {
30+
return safariMatch[1];
31+
}
32+
return null;
33+
})
34+
.filter((line) => line && pathOfChunks.some((path) => line.includes(path)))
35+
.length;
36+
}
37+
38+
/**
39+
* Add nonce to style elements that do not have it yet for the selected chunks
40+
*/
41+
export function addNonceToStyles(nonce: string, chunksWithoutNonce: string[]) {
42+
const addNonceToStyle = (element: Node) => {
43+
const isStyleElement = element instanceof HTMLStyleElement;
44+
if (!isStyleElement) {
45+
return;
46+
}
47+
48+
const hasNonce = element.hasAttribute("nonce");
49+
if (hasNonce) {
50+
return;
51+
}
52+
53+
if (!isSelectedChunk(chunksWithoutNonce)) {
54+
return;
55+
}
56+
57+
element.setAttribute("nonce", nonce);
58+
};
59+
60+
Node.prototype.appendChild = function <T extends Node>(node: T): T {
61+
addNonceToStyle(node);
62+
return originalAppendChild.call(this, node) as T;
63+
};
64+
65+
Node.prototype.insertBefore = function <T extends Node>(
66+
node: T,
67+
referenceNode: Node | null,
68+
): T {
69+
addNonceToStyle(node);
70+
return originalInsertBefore.call(this, node, referenceNode) as T;
71+
};
72+
}

vite.config.mts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import vue from "@vitejs/plugin-vue";
33
import { defineConfig } from "vite";
44
import csp from "vite-plugin-csp-guard";
55

6+
process.env.VITE_NONCE = Math.random().toString(36).slice(2);
7+
68
// https://vitejs.dev/config/
79
export default defineConfig({
10+
html: {
11+
cspNonce: process.env.VITE_NONCE,
12+
},
813
plugins: [
914
vue(),
1015
csp({
@@ -13,8 +18,8 @@ export default defineConfig({
1318
outlierSupport: ["vue"],
1419
},
1520
policy: {
16-
"style-src": ["'self'", "'unsafe-inline'"], // todo remove when naive-ui will be fixed
17-
"style-src-elem": ["'unsafe-inline'"], // todo remove when naive-ui will be fixed
21+
"style-src": ["'self'"],
22+
"style-src-elem": ["'self'", `'nonce-${process.env.VITE_NONCE}'`],
1823
"img-src": ["data:", "blob:"],
1924
"script-src": ["'wasm-unsafe-eval'"],
2025
"script-src-elem": ["https://analytics.umami.is/script.js"],
@@ -30,6 +35,19 @@ export default defineConfig({
3035
build: {
3136
target: "es2022",
3237
assetsInlineLimit: 0,
38+
rollupOptions: {
39+
output: {
40+
manualChunks(id) {
41+
// [tag-nonce]
42+
// Naive-ui doesn't support nonce, part of logic with workaround
43+
// Search by tag in the code
44+
const nativeUiPath = process.cwd() + "/node_modules/naive-ui/";
45+
if (id.startsWith(nativeUiPath)) {
46+
return "naive-ui"; // https://github.com/tusen-ai/naive-ui/issues/6356
47+
}
48+
},
49+
},
50+
},
3351
},
3452
resolve: {
3553
alias: {

0 commit comments

Comments
 (0)