Skip to content

Commit

Permalink
fix: fs serve only edit pathname (fixes vitejs#9148)
Browse files Browse the repository at this point in the history
  • Loading branch information
sapphi-red committed Jul 17, 2022
1 parent e6f3b02 commit df67e8b
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 18 deletions.
48 changes: 30 additions & 18 deletions packages/vite/src/node/server/middlewares/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,36 +80,40 @@ export function serveStaticMiddleware(
return next()
}

const url = decodeURIComponent(req.url!)
const url = new URL(req.url!, 'http://example.com')
const pathname = getDecodedPathname(url)

// apply aliases to static requests as well
let redirected: string | undefined
let redirectedPathname: string | undefined
for (const { find, replacement } of server.config.resolve.alias) {
const matches =
typeof find === 'string' ? url.startsWith(find) : find.test(url)
typeof find === 'string'
? pathname.startsWith(find)
: find.test(pathname)
if (matches) {
redirected = url.replace(find, replacement)
redirectedPathname = pathname.replace(find, replacement)
break
}
}
if (redirected) {
if (redirectedPathname) {
// dir is pre-normalized to posix style
if (redirected.startsWith(dir)) {
redirected = redirected.slice(dir.length)
if (redirectedPathname.startsWith(dir)) {
redirectedPathname = redirectedPathname.slice(dir.length)
}
}

const resolvedUrl = redirected || url
let fileUrl = path.resolve(dir, resolvedUrl.replace(/^\//, ''))
if (resolvedUrl.endsWith('/') && !fileUrl.endsWith('/')) {
const resolvedPathname = redirectedPathname || pathname
let fileUrl = path.resolve(dir, resolvedPathname.replace(/^\//, ''))
if (resolvedPathname.endsWith('/') && !fileUrl.endsWith('/')) {
fileUrl = fileUrl + '/'
}
if (!ensureServingAccess(fileUrl, server, res, next)) {
return
}

if (redirected) {
req.url = encodeURIComponent(redirected)
if (redirectedPathname) {
url.pathname = encodeURIComponent(redirectedPathname)
req.url = url.href.slice(url.origin.length)
}

serve(req, res, next)
Expand All @@ -123,16 +127,17 @@ export function serveRawFsMiddleware(

// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteServeRawFsMiddleware(req, res, next) {
let url = decodeURIComponent(req.url!)
const url = new URL(req.url!, 'http://example.com')
// In some cases (e.g. linked monorepos) files outside of root will
// reference assets that are also out of served root. In such cases
// the paths are rewritten to `/@fs/` prefixed paths and must be served by
// searching based from fs root.
if (url.startsWith(FS_PREFIX)) {
if (url.pathname.startsWith(FS_PREFIX)) {
const pathname = getDecodedPathname(url)
// restrict files outside of `fs.allow`
if (
!ensureServingAccess(
slash(path.resolve(fsPathFromId(url))),
slash(path.resolve(fsPathFromId(pathname))),
server,
res,
next
Expand All @@ -141,17 +146,24 @@ export function serveRawFsMiddleware(
return
}

url = url.slice(FS_PREFIX.length)
if (isWindows) url = url.replace(/^[A-Z]:/i, '')
let newPathname = pathname.slice(FS_PREFIX.length)
if (isWindows) newPathname = newPathname.replace(/^[A-Z]:/i, '')

req.url = encodeURIComponent(url)
url.pathname = encodeURIComponent(newPathname)
req.url = url.href.slice(url.origin.length)
serveFromRoot(req, res, next)
} else {
next()
}
}
}

function getDecodedPathname(url: URL) {
return url.pathname.includes('%')
? decodeURIComponent(url.pathname)
: url.pathname
}

const _matchOptions = { matchBase: true }

export function isFileServingAllowed(
Expand Down
10 changes: 10 additions & 0 deletions playground/fs-serve/__tests__/fs-serve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ describe.runIf(isServe)('main', () => {
expect(await page.textContent('.safe-fetch-status')).toBe('200')
})

test('safe fetch with query', async () => {
expect(await page.textContent('.safe-fetch-query')).toMatch('KEY=safe')
expect(await page.textContent('.safe-fetch-query-status')).toBe('200')
})

test('safe fetch with special characters', async () => {
expect(
await page.textContent('.safe-fetch-subdir-special-characters')
Expand Down Expand Up @@ -52,6 +57,11 @@ describe.runIf(isServe)('main', () => {
expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
})

test('safe fs fetch', async () => {
expect(await page.textContent('.safe-fs-fetch-query')).toBe(stringified)
expect(await page.textContent('.safe-fs-fetch-query-status')).toBe('200')
})

test('safe fs fetch with special characters', async () => {
expect(await page.textContent('.safe-fs-fetch-special-characters')).toBe(
stringified
Expand Down
25 changes: 25 additions & 0 deletions playground/fs-serve/root/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ <h2>Normal Import</h2>
<h2>Safe Fetch</h2>
<pre class="safe-fetch-status"></pre>
<pre class="safe-fetch"></pre>
<pre class="safe-fetch-query-status"></pre>
<pre class="safe-fetch-query"></pre>

<h2>Safe Fetch Subdirectory</h2>
<pre class="safe-fetch-subdir-status"></pre>
Expand All @@ -25,6 +27,8 @@ <h2>Unsafe Fetch</h2>
<h2>Safe /@fs/ Fetch</h2>
<pre class="safe-fs-fetch-status"></pre>
<pre class="safe-fs-fetch"></pre>
<pre class="safe-fs-fetch-query-status"></pre>
<pre class="safe-fs-fetch-query"></pre>
<pre class="safe-fs-fetch-special-characters-status"></pre>
<pre class="safe-fs-fetch-special-characters"></pre>

Expand Down Expand Up @@ -58,6 +62,17 @@ <h2>Denied</h2>
.then((data) => {
text('.safe-fetch', JSON.stringify(data))
})

// inside allowed dir with query, safe fetch
fetch('/src/safe.txt?query')
.then((r) => {
text('.safe-fetch-query-status', r.status)
return r.text()
})
.then((data) => {
text('.safe-fetch-query', JSON.stringify(data))
})

// inside allowed dir, safe fetch
fetch('/src/subdir/safe.txt')
.then((r) => {
Expand Down Expand Up @@ -127,6 +142,16 @@ <h2>Denied</h2>
text('.safe-fs-fetch', JSON.stringify(data))
})

// imported before with query, should be treated as safe
fetch('/@fs/' + ROOT + '/safe.json?query')
.then((r) => {
text('.safe-fs-fetch-query-status', r.status)
return r.json()
})
.then((data) => {
text('.safe-fs-fetch-query', JSON.stringify(data))
})

// not imported before, outside of root, treated as unsafe
fetch('/@fs/' + ROOT + '/unsafe.json')
.then((r) => {
Expand Down

0 comments on commit df67e8b

Please sign in to comment.