From 2f468bb33c9f55efa703363fefa0aa99dbf63929 Mon Sep 17 00:00:00 2001 From: Dominik G Date: Fri, 19 Aug 2022 14:50:38 +0200 Subject: [PATCH] fix: sanitize asset filenames (#9737) --- packages/vite/src/node/plugins/asset.ts | 19 ++++++++++++- playground/assets-sanitize/+circle.svg | 3 +++ .../__tests__/assets-sanitize.spec.ts | 27 +++++++++++++++++++ playground/assets-sanitize/_circle.svg | 3 +++ playground/assets-sanitize/index.html | 11 ++++++++ playground/assets-sanitize/index.js | 9 +++++++ playground/assets-sanitize/package.json | 11 ++++++++ playground/assets-sanitize/vite.config.js | 11 ++++++++ 8 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 playground/assets-sanitize/+circle.svg create mode 100644 playground/assets-sanitize/__tests__/assets-sanitize.spec.ts create mode 100644 playground/assets-sanitize/_circle.svg create mode 100644 playground/assets-sanitize/index.html create mode 100644 playground/assets-sanitize/index.js create mode 100644 playground/assets-sanitize/package.json create mode 100644 playground/assets-sanitize/vite.config.js diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index d3fc79475..c018ba564 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -342,7 +342,7 @@ export function assetFileNamesToFileName( return hash case '[name]': - return name + return sanitizeFileName(name) } throw new Error( `invalid placeholder ${placeholder} in assetFileNames "${assetFileNames}"` @@ -353,6 +353,23 @@ export function assetFileNamesToFileName( return fileName } +// taken from https://github.com/rollup/rollup/blob/a8647dac0fe46c86183be8596ef7de25bc5b4e4b/src/utils/sanitizeFileName.ts +// https://datatracker.ietf.org/doc/html/rfc2396 +// eslint-disable-next-line no-control-regex +const INVALID_CHAR_REGEX = /[\x00-\x1F\x7F<>*#"{}|^[\]`;?:&=+$,]/g +const DRIVE_LETTER_REGEX = /^[a-z]:/i +function sanitizeFileName(name: string): string { + const match = DRIVE_LETTER_REGEX.exec(name) + const driveLetter = match ? match[0] : '' + + // A `:` is only allowed as part of a windows drive letter (ex: C:\foo) + // Otherwise, avoid them because they can refer to NTFS alternate data streams. + return ( + driveLetter + + name.substr(driveLetter.length).replace(INVALID_CHAR_REGEX, '_') + ) +} + export const publicAssetUrlCache = new WeakMap< ResolvedConfig, // hash -> url diff --git a/playground/assets-sanitize/+circle.svg b/playground/assets-sanitize/+circle.svg new file mode 100644 index 000000000..81ff39ee1 --- /dev/null +++ b/playground/assets-sanitize/+circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/playground/assets-sanitize/__tests__/assets-sanitize.spec.ts b/playground/assets-sanitize/__tests__/assets-sanitize.spec.ts new file mode 100644 index 000000000..fc9c1ad8c --- /dev/null +++ b/playground/assets-sanitize/__tests__/assets-sanitize.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from 'vitest' +import { getBg, isBuild, page, readManifest } from '~utils' + +if (!isBuild) { + test('importing asset with special char in filename works in dev', async () => { + expect(await getBg('.plus-circle')).toContain('+circle.svg') + expect(await page.textContent('.plus-circle')).toMatch('+circle.svg') + expect(await getBg('.underscore-circle')).toContain('_circle.svg') + expect(await page.textContent('.underscore-circle')).toMatch('_circle.svg') + }) +} else { + test('importing asset with special char in filename works in build', async () => { + const manifest = readManifest() + const plusCircleAsset = manifest['+circle.svg'].file + const underscoreCircleAsset = manifest['_circle.svg'].file + expect(await getBg('.plus-circle')).toMatch(plusCircleAsset) + expect(await page.textContent('.plus-circle')).toMatch(plusCircleAsset) + expect(await getBg('.underscore-circle')).toMatch(underscoreCircleAsset) + expect(await page.textContent('.underscore-circle')).toMatch( + underscoreCircleAsset + ) + expect(plusCircleAsset).toMatch('/_circle') + expect(underscoreCircleAsset).toMatch('/_circle') + expect(plusCircleAsset).not.toEqual(underscoreCircleAsset) + expect(Object.keys(manifest).length).toBe(3) // 2 svg, 1 index.js + }) +} diff --git a/playground/assets-sanitize/_circle.svg b/playground/assets-sanitize/_circle.svg new file mode 100644 index 000000000..f8e310c61 --- /dev/null +++ b/playground/assets-sanitize/_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/playground/assets-sanitize/index.html b/playground/assets-sanitize/index.html new file mode 100644 index 000000000..e4b4913ca --- /dev/null +++ b/playground/assets-sanitize/index.html @@ -0,0 +1,11 @@ + + +

test elements below should show circles and their url

+
+
diff --git a/playground/assets-sanitize/index.js b/playground/assets-sanitize/index.js new file mode 100644 index 000000000..bac3b3b83 --- /dev/null +++ b/playground/assets-sanitize/index.js @@ -0,0 +1,9 @@ +import plusCircle from './+circle.svg' +import underscoreCircle from './_circle.svg' +function setData(classname, file) { + const el = document.body.querySelector(classname) + el.style.backgroundImage = `url(${file})` + el.textContent = file +} +setData('.plus-circle', plusCircle) +setData('.underscore-circle', underscoreCircle) diff --git a/playground/assets-sanitize/package.json b/playground/assets-sanitize/package.json new file mode 100644 index 000000000..3ade78a2b --- /dev/null +++ b/playground/assets-sanitize/package.json @@ -0,0 +1,11 @@ +{ + "name": "test-assets-sanitize", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "preview": "vite preview" + } +} diff --git a/playground/assets-sanitize/vite.config.js b/playground/assets-sanitize/vite.config.js new file mode 100644 index 000000000..0e365a953 --- /dev/null +++ b/playground/assets-sanitize/vite.config.js @@ -0,0 +1,11 @@ +const { defineConfig } = require('vite') + +module.exports = defineConfig({ + build: { + //speed up build + minify: false, + target: 'esnext', + assetsInlineLimit: 0, + manifest: true + } +})