Skip to content

Commit 8983f17

Browse files
authored
fix(dev): prevent extraneous file requests (#15279)
1 parent 2920a2e commit 8983f17

File tree

12 files changed

+214
-0
lines changed

12 files changed

+214
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes an issue where the dev server would serve files like `/README.md` from the project root when they shouldn't be accessible. A new route guard middleware now blocks direct URL access to files that exist outside of `srcDir` and `publicDir`, returning a 404 instead.

packages/astro/src/vite-plugin-astro-server/plugin.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { baseMiddleware } from './base.js';
2929
import { createController } from './controller.js';
3030
import { recordServerError } from './error.js';
3131
import { setRouteError } from './server-state.js';
32+
import { routeGuardMiddleware } from './route-guard.js';
3233
import { trailingSlashMiddleware } from './trailing-slash.js';
3334
import { sessionConfigToManifest } from '../core/session/utils.js';
3435

@@ -98,6 +99,11 @@ export default function createVitePluginAstroServer({
9899
route: '',
99100
handle: trailingSlashMiddleware(settings),
100101
});
102+
// Prevent serving files outside srcDir/publicDir (e.g., /README.md at project root)
103+
viteServer.middlewares.stack.unshift({
104+
route: '',
105+
handle: routeGuardMiddleware(settings),
106+
});
101107

102108
// Note that this function has a name so other middleware can find it.
103109
viteServer.middlewares.use(async function astroDevHandler(request, response) {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as fs from 'node:fs';
2+
import type * as vite from 'vite';
3+
import type { AstroSettings } from '../types/astro.js';
4+
import { notFoundTemplate } from '../template/4xx.js';
5+
import { writeHtmlResponse } from './response.js';
6+
7+
// Vite internal prefixes that should always be allowed through
8+
const VITE_INTERNAL_PREFIXES = [
9+
'/@vite/',
10+
'/@fs/',
11+
'/@id/',
12+
'/__vite',
13+
'/@react-refresh',
14+
'/node_modules/',
15+
'/.astro/',
16+
];
17+
18+
/**
19+
* Middleware that prevents Vite from serving files that exist outside
20+
* of srcDir and publicDir when accessed via direct URL navigation.
21+
*
22+
* This fixes the issue where files like /README.md are served
23+
* when they exist at the project root but aren't part of Astro's routing.
24+
*/
25+
export function routeGuardMiddleware(settings: AstroSettings): vite.Connect.NextHandleFunction {
26+
const { config } = settings;
27+
28+
return function devRouteGuard(req, res, next) {
29+
const url = req.url;
30+
if (!url) {
31+
return next();
32+
}
33+
34+
// Only intercept requests that look like browser navigation (HTML requests)
35+
// Let all other requests through (JS modules, assets, Vite transforms, etc.)
36+
const accept = req.headers.accept || '';
37+
if (!accept.includes('text/html')) {
38+
return next();
39+
}
40+
41+
let pathname: string;
42+
try {
43+
pathname = decodeURI(new URL(url, 'http://localhost').pathname);
44+
} catch {
45+
// Malformed URI, let other middleware handle it
46+
return next();
47+
}
48+
49+
// Always allow Vite internal paths through
50+
if (VITE_INTERNAL_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
51+
return next();
52+
}
53+
54+
// Always allow requests with query params (Vite transform requests like ?url, ?raw)
55+
if (url.includes('?')) {
56+
return next();
57+
}
58+
59+
// Check if the file exists in publicDir - allow if so
60+
const publicFilePath = new URL('.' + pathname, config.publicDir);
61+
if (fs.existsSync(publicFilePath)) {
62+
return next();
63+
}
64+
65+
// Check if the file exists in srcDir - allow if so (potential route)
66+
const srcFilePath = new URL('.' + pathname, config.srcDir);
67+
if (fs.existsSync(srcFilePath)) {
68+
return next();
69+
}
70+
71+
// Check if the file exists at project root (outside srcDir/publicDir)
72+
const rootFilePath = new URL('.' + pathname, config.root);
73+
if (fs.existsSync(rootFilePath)) {
74+
// File exists at root but not in srcDir or publicDir - block it
75+
const html = notFoundTemplate(pathname);
76+
return writeHtmlResponse(res, 404, html);
77+
}
78+
79+
// File doesn't exist anywhere, let other middleware handle it
80+
return next();
81+
};
82+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
MIT License - This file should NOT be accessible via /LICENSE
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Test README
2+
3+
This file should NOT be accessible via /README.md
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"secret": "this-should-not-be-served"
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@test/route-guard",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"astro": "workspace:*"
7+
}
8+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
User-agent: *
2+
Allow: /
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
---
3+
# About Page
4+
5+
This is a valid markdown page.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
---
3+
<html>
4+
<head><title>Home</title></head>
5+
<body><h1>Home Page</h1></body>
6+
</html>

0 commit comments

Comments
 (0)