-
Notifications
You must be signed in to change notification settings - Fork 66.8k
Expand file tree
/
Copy pathhandle-invalid-paths.ts
More file actions
105 lines (93 loc) · 3 KB
/
handle-invalid-paths.ts
File metadata and controls
105 lines (93 loc) · 3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import type { Response, NextFunction } from 'express'
import { defaultCacheControl } from '@/frame/middleware/cache-control'
import { ExtendedRequest } from '@/types'
// We'll check if the current request path is one of these, or ends with
// one of these.
// These are clearly intentional "guesses" made by some sort of
// pen-testing bot.
const JUNK_STARTS = ['///', '/\\', '/\\.']
const JUNK_ENDS = [
'/package.json',
'/package-lock.json',
'/etc/passwd',
'/Gemfile',
'/Gemfile.lock',
'/WEB-INF/web.xml',
'/WEB-INF/web.xml%C0%80.jsp',
]
const JUNK_PATHS = new Set([
...JUNK_ENDS,
'/env',
'/xmlrpc.php',
'/wp-login.php',
'/README.md',
'/server.js',
'/.git',
'/_next',
])
// Basename is the last token of the path when split by `/`.
// For example `/foo/bar/baz` has a basename of `baz`.
const JUNK_BASENAMES = new Set([
// E.g. /en/code-security/.env
'.env',
])
function isJunkPath(path: string) {
if (JUNK_PATHS.has(path)) return true
for (const junkPath of JUNK_STARTS) {
if (path.startsWith(junkPath)) {
return true
}
}
for (const junkPath of JUNK_ENDS) {
if (path.endsWith(junkPath)) {
return true
}
}
const basename = path.split('/').pop()
// E.g. `/billing/.env.local` or `/billing/.env_sample`
if (basename && /^\.env(.|_)[\w.]+/.test(basename)) return true
if (basename && JUNK_BASENAMES.has(basename)) return true
// Prevent various malicious injection attacks targeting Next.js
if (path.match(/^\/_next[^/]/) || path === '/_next/data' || path === '/_next/data/') {
return true
}
// We currently don't use next/image for any images.
// This could change in the future but right now can just 404 on these
// so we don't have to deal with any other errors.
if (path.startsWith('/_next/image')) {
return true
}
return false
}
export default function handleInvalidPaths(
req: ExtendedRequest,
res: Response,
next: NextFunction,
) {
if (isJunkPath(req.path)) {
// We can all the CDN to cache these responses because they're
// they're not going to suddenly work in the next deployment.
defaultCacheControl(res)
res.status(404).type('text').send('Not found')
return
}
if (req.path.endsWith('/index.md')) {
defaultCacheControl(res)
// The originalUrl is the full URL including query string.
// E.g. `/en/foo.md?bar=baz`
const newUrl = req.originalUrl.replace(req.path, req.path.replace(/\/index\.md$/, ''))
return res.redirect(newUrl)
} else if (req.path.endsWith('.md')) {
// encode the query params but also make them pretty so we can see
// them as `/` and `@` in the address bar
// e.g. /api/article/body?pathname=/en/enterprise-server@3.16/admin...
// NOT: /api/article/body?pathname=%2Fen%2Fenterprise-server%403.16%2Fadmin...
const encodedPath = encodeURIComponent(req.path.replace(/\.md$/, ''))
.replace(/%2F/g, '/')
.replace(/%40/g, '@')
const newUrl = `/api/article/body?pathname=${encodedPath}`
res.redirect(newUrl)
return
}
return next()
}