Skip to content

Commit 1567e8c

Browse files
Normalize static file paths before evaluating dotfile access rules (#15763)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 02e24d9 commit 1567e8c

4 files changed

Lines changed: 49 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/node': patch
3+
---
4+
5+
Normalizes static file paths before evaluating dotfile access rules for improved consistency

packages/integrations/node/src/serve-static.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ export function createStaticHandler(
120120
// app.removeBase sometimes returns a path without a leading slash
121121
pathname = prependForwardSlash(app.removeBase(pathname));
122122

123-
const stream = send(req, pathname, {
123+
const normalizedPathname = path.posix.normalize(pathname);
124+
const stream = send(req, normalizedPathname, {
124125
root: client,
125-
dotfiles: pathname.startsWith('/.well-known/') ? 'allow' : 'deny',
126+
dotfiles: normalizedPathname.startsWith('/.well-known/') ? 'allow' : 'deny',
126127
});
127128

128129
let forwardError = false;
@@ -139,7 +140,7 @@ export function createStaticHandler(
139140
});
140141
stream.on('headers', (_res: ServerResponse) => {
141142
// assets in dist/_astro are hashed and should get the immutable header
142-
if (pathname.startsWith(`/${app.manifest.assetsDir}/`)) {
143+
if (normalizedPathname.startsWith(`/${app.manifest.assetsDir}/`)) {
143144
// This is the "far future" cache header, used for static files whose name includes their digest hash.
144145
// 1 year (31,536,000 seconds) is convention.
145146
// Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
should-not-serve

packages/integrations/node/test/well-known-locations.test.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as assert from 'node:assert/strict';
22
import { after, before, describe, it } from 'node:test';
33
import nodejs from '../dist/index.js';
4-
import { loadFixture } from './test-utils.js';
4+
import { createRequestAndResponse, loadFixture } from './test-utils.js';
55

66
describe('test URIs beginning with a dot', () => {
77
/** @type {import('./test-utils').Fixture} */
@@ -43,4 +43,42 @@ describe('test URIs beginning with a dot', () => {
4343
assert.equal(res.status, 404);
4444
});
4545
});
46+
47+
describe('dotfile access via unnormalized paths', async () => {
48+
it('denies dotfile access when path contains .well-known/../ traversal', async () => {
49+
const { handler } = await import('./fixtures/well-known-locations/dist/server/entry.mjs');
50+
const { req, res, done } = createRequestAndResponse({
51+
method: 'GET',
52+
url: '/.well-known/../.hidden-file',
53+
});
54+
55+
handler(req, res);
56+
req.send();
57+
58+
await done;
59+
assert.notEqual(
60+
res.statusCode,
61+
200,
62+
'dotfile should not be served via .well-known path traversal',
63+
);
64+
});
65+
66+
it('denies dotfolder file access when path contains .well-known/../ traversal', async () => {
67+
const { handler } = await import('./fixtures/well-known-locations/dist/server/entry.mjs');
68+
const { req, res, done } = createRequestAndResponse({
69+
method: 'GET',
70+
url: '/.well-known/../.hidden/file.json',
71+
});
72+
73+
handler(req, res);
74+
req.send();
75+
76+
await done;
77+
assert.notEqual(
78+
res.statusCode,
79+
200,
80+
'dotfolder file should not be served via .well-known path traversal',
81+
);
82+
});
83+
});
4684
});

0 commit comments

Comments
 (0)