This commit is contained in:
2026-06-28 16:22:09 +01:00
parent 109eeebfb1
commit 8b24b12ffb
+76
View File
@@ -1281,6 +1281,31 @@ function isProtectedDataPath(req) {
} }
} }
function rawRequestPath(req) {
return String(req.url || '/').split(/[?#]/, 1)[0] || '/'
}
function hasUnsafeProtectedPath(req) {
const rawPath = rawRequestPath(req)
const lower = rawPath.toLowerCase()
const protectedPrefix =
lower === '/api' ||
lower.startsWith('/api/') ||
lower.startsWith('/api%2f') ||
lower.startsWith('/api%5c') ||
lower === '/data' ||
lower.startsWith('/data/') ||
lower.startsWith('/data%2f') ||
lower.startsWith('/data%5c')
if (!protectedPrefix) return false
const canonicalPrefix = rawPath.startsWith('/api/') || rawPath.startsWith('/data/')
if (!canonicalPrefix) return true
return /%(?:00|2e|2f|5c)/i.test(rawPath)
}
function isSiteSessionBootstrapPath(req) { function isSiteSessionBootstrapPath(req) {
if (!req.url) return false if (!req.url) return false
try { try {
@@ -2536,6 +2561,45 @@ ${urls.map((url) => ` <url>
}) })
} }
function shouldBypassSpaFallback(requestPath) {
const lower = requestPath.toLowerCase()
const basename = path.posix.basename(lower)
const ext = path.posix.extname(lower)
if (lower.startsWith('/.')) return true
if (lower.includes('/.git/') || lower.includes('/.svn/') || lower.includes('/.hg/')) return true
if (lower.startsWith('/api') || lower.startsWith('/data')) return true
if (
[
'.cjs',
'.mjs',
'.js',
'.jsx',
'.ts',
'.tsx',
'.json',
'.lock',
'.toml',
'.yaml',
'.yml',
'.env',
'.sqlite',
'.db',
'.sql',
'.zip',
'.gz',
'.tgz',
'.tar',
'.bak',
].includes(ext)
) {
return true
}
return /(?:^|[._-])(?:env|backup|dump|secret|credential|token|database|sqlite|db)(?:$|[._-])/.test(basename)
}
function serveStatic(req, res) { function serveStatic(req, res) {
let requestPath = '/' let requestPath = '/'
try { try {
@@ -2553,6 +2617,13 @@ function serveStatic(req, res) {
fs.stat(filePath, (error, stat) => { fs.stat(filePath, (error, stat) => {
if (error || !stat.isFile()) { if (error || !stat.isFile()) {
if (shouldBypassSpaFallback(requestPath)) {
return send(res, 404, 'Not found', {
'content-type': 'text/plain; charset=utf-8',
'cache-control': 'no-store',
})
}
fs.readFile(path.join(DIST_DIR, 'index.html'), (indexError, indexData) => { fs.readFile(path.join(DIST_DIR, 'index.html'), (indexError, indexData) => {
if (indexError) { if (indexError) {
return send(res, 404, 'Build not found. Run npm run build first.', { return send(res, 404, 'Build not found. Run npm run build first.', {
@@ -2865,6 +2936,11 @@ function serveReplayMinimap(req, res) {
} }
const server = http.createServer((req, res) => { const server = http.createServer((req, res) => {
if (hasUnsafeProtectedPath(req)) {
sendJson(res, 404, { error: 'API route not found' })
return
}
if (req.method === 'GET' && req.url === '/robots.txt') { if (req.method === 'GET' && req.url === '/robots.txt') {
sendRobotsTxt(req, res) sendRobotsTxt(req, res)
return return