Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
import { isFileInTargetPath } from '../static'
import { isFileInTargetPath, looksLikeWindowsShortNamePath } from '../static'

describe('isFileInTargetPath', () => {
const cases = {
Expand Down Expand Up @@ -27,3 +27,38 @@ describe('isFileInTargetPath', () => {
}
}
})

describe('looksLikeWindowsShortNamePath', () => {
const shortNamePaths = [
// classic 8.3 short names
'C:/PROGRA~1/x',
'C:/PROGRA~1',
'C:/LONGFI~1.TXT',
'C:/MICROS~2/foo',
// short-name-looking directory ancestor, not just the basename
'C:/foo/DOCUME~1/bar.js',
]
const legitimateTildePaths = [
// real-world case from the reported issue: `~` not followed by a digit
'C:/project/dist/0~rslib-runtime.js',
// ancestor directory containing a tilde that isn't short-name shaped
'C:/Users/foo~bar/project/index.js',
// tilde-prefixed name with no short-name-style prefix/digit suffix
'C:/Users/foo/~backup/index.js',
// prefix longer than the 6 characters a short name can have
'C:/project/confirmations~2/index.js',
// no tilde at all
'C:/Users/foo/project/index.js',
]

for (const filePath of shortNamePaths) {
test(`looksLikeWindowsShortNamePath("${filePath}") is true`, () => {
expect(looksLikeWindowsShortNamePath(filePath)).toBe(true)
})
}
for (const filePath of legitimateTildePaths) {
test(`looksLikeWindowsShortNamePath("${filePath}") is false`, () => {
expect(looksLikeWindowsShortNamePath(filePath)).toBe(false)
})
}
})
27 changes: 24 additions & 3 deletions packages/vite/src/node/server/middlewares/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,24 @@ export function isFileInTargetPath(
}

const windowsDriveRE = /^[A-Z]:/i
// A Windows 8.3 "short name" segment looks like `NAME~1` or `NAME~1.EXT`: at
// most 6 non-`~`/`.` characters, a `~`, then digits, within a single path
// segment. Matching only this shape (rather than any `~`) still blocks the
// short-name aliasing bypass while allowing filenames that merely contain a
// tilde, e.g. `0~rslib-runtime.js`.
const windowsShortNameSegmentRE = /^[^~.]{1,6}~\d+(?:\.[^~.]{0,3})?$/

/**
* Warning: parameters are not validated, only works with normalized absolute paths
*/
export function looksLikeWindowsShortNamePath(filePath: string): boolean {
return (
filePath.includes('~') &&
filePath
.split('/')
.some((segment) => windowsShortNameSegmentRE.test(segment))
)
}

/**
* Warning: parameters are not validated, only works with normalized absolute paths
Expand All @@ -299,9 +317,12 @@ export function isFileLoadingAllowed(

if (!fs.strict) return true

if (isWindows && filePath.includes('~')) {
// `~` is used for Windows 8.3 short names, which can be used to bypass the check.
// While is it valid to have files with `~` in the path, we disallow it to be safe.
if (isWindows && looksLikeWindowsShortNamePath(filePath)) {
// Windows 8.3 short names (e.g. `PROGRA~1`) can alias a different long
// path and can be used to bypass the check.
// While it is valid to have files named similar to automatically generated
// short names, it is unlikely that a user would create them, so we
// disallow them to be safe.
return false
}

Expand Down
Loading