Skip to content

Commit fb8de48

Browse files
handle repeated slash scanner posts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
1 parent c98aee3 commit fb8de48

2 files changed

Lines changed: 94 additions & 8 deletions

File tree

epicshop/patch-workshop-app.js

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ const defaultServerBuildPath = path.join(
1212
'server',
1313
'index.js',
1414
)
15+
const defaultServerRuntimePath = path.join(
16+
here,
17+
'node_modules',
18+
'@epic-web',
19+
'workshop-app',
20+
'dist',
21+
'server',
22+
'index.js',
23+
)
1524

1625
const notFoundActionFunctionName = 'action$splatNotFound'
1726
const notFoundActionFunction = `async function ${notFoundActionFunctionName}() {
@@ -50,6 +59,21 @@ const routeManifestNeedle =
5059
const patchedRouteManifestNeedle =
5160
'"routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": true,'
5261

62+
const repeatedSlashMiddleware = `app.use((req, res, next) => {
63+
const requestPath = req.originalUrl.split("?")[0];
64+
if (req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS" && /^\\/{2,}$/.test(requestPath)) {
65+
res.status(404).send("Not found");
66+
return;
67+
}
68+
next();
69+
});
70+
`
71+
72+
const requestContextMiddlewareNeedle =
73+
'app.use((_req, _res, next) => requestContext.run({}, next));\n'
74+
75+
const patchedRequestContextMiddlewareNeedle = `${requestContextMiddlewareNeedle}${repeatedSlashMiddleware}`
76+
5377
export function patchWorkshopAppServerBuild(source) {
5478
const hasActionFunction = source.includes(
5579
`async function ${notFoundActionFunctionName}()`,
@@ -111,20 +135,57 @@ export function patchWorkshopAppServerBuild(source) {
111135
}
112136
}
113137

138+
export function patchWorkshopAppServerRuntime(source) {
139+
if (source.includes(repeatedSlashMiddleware)) {
140+
return { patched: false, source }
141+
}
142+
143+
if (!source.includes(requestContextMiddlewareNeedle)) {
144+
throw new Error(
145+
'Could not find the workshop-app request context middleware to patch.',
146+
)
147+
}
148+
149+
return {
150+
patched: true,
151+
source: source.replace(
152+
requestContextMiddlewareNeedle,
153+
patchedRequestContextMiddlewareNeedle,
154+
),
155+
}
156+
}
157+
114158
export async function patchWorkshopApp({
115159
serverBuildPath = defaultServerBuildPath,
160+
serverRuntimePath = defaultServerRuntimePath,
116161
} = {}) {
117-
const source = await fs.readFile(serverBuildPath, 'utf8')
118-
const result = patchWorkshopAppServerBuild(source)
119-
120-
if (result.patched) {
121-
await fs.writeFile(serverBuildPath, result.source)
162+
const [serverBuildSource, serverRuntimeSource] = await Promise.all([
163+
fs.readFile(serverBuildPath, 'utf8'),
164+
fs.readFile(serverRuntimePath, 'utf8'),
165+
])
166+
const serverBuildResult = patchWorkshopAppServerBuild(serverBuildSource)
167+
const serverRuntimeResult = patchWorkshopAppServerRuntime(serverRuntimeSource)
168+
169+
await Promise.all([
170+
serverBuildResult.patched
171+
? fs.writeFile(serverBuildPath, serverBuildResult.source)
172+
: null,
173+
serverRuntimeResult.patched
174+
? fs.writeFile(serverRuntimePath, serverRuntimeResult.source)
175+
: null,
176+
])
177+
178+
if (serverBuildResult.patched || serverRuntimeResult.patched) {
122179
console.log(
123180
'Patched @epic-web/workshop-app splat route to return a normal 404 for POST scanner traffic.',
124181
)
125182
}
126183

127-
return result
184+
return {
185+
patched: serverBuildResult.patched || serverRuntimeResult.patched,
186+
serverBuild: serverBuildResult,
187+
serverRuntime: serverRuntimeResult,
188+
}
128189
}
129190

130191
if (import.meta.url === pathToFileURL(process.argv[1]).href) {

epicshop/patch-workshop-app.test.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import test from 'node:test'
33
import {
44
patchWorkshopApp,
55
patchWorkshopAppServerBuild,
6+
patchWorkshopAppServerRuntime,
67
} from './patch-workshop-app.js'
78

89
const unpatchedServerBuildFixture = `async function loader$L({
@@ -38,6 +39,13 @@ const route1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
3839
const serverManifest = { "routes": { "routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": false, "hasLoader": true } } };
3940
`
4041

42+
const unpatchedServerRuntimeFixture = `app.use((_req, _res, next) => requestContext.run({}, next));
43+
app.options("*splat", (_req, res) => {
44+
res.set("Allow", "GET, HEAD, POST, OPTIONS");
45+
res.sendStatus(204);
46+
});
47+
`
48+
4149
test('adds a 404 action to the workshop-app splat route module', () => {
4250
const result = patchWorkshopAppServerBuild(unpatchedServerBuildFixture)
4351

@@ -62,10 +70,27 @@ test('fails loudly when the expected route shape changes', () => {
6270
)
6371
})
6472

73+
test('adds a narrow repeated-slash POST guard before React Router', () => {
74+
const result = patchWorkshopAppServerRuntime(unpatchedServerRuntimeFixture)
75+
76+
assert.equal(result.patched, true)
77+
assert.match(result.source, /requestPath = req\.originalUrl\.split/)
78+
assert.match(result.source, /\^\\\/\{2,\}\$/)
79+
})
80+
81+
test('does not modify an already patched server runtime', () => {
82+
const firstResult = patchWorkshopAppServerRuntime(unpatchedServerRuntimeFixture)
83+
const secondResult = patchWorkshopAppServerRuntime(firstResult.source)
84+
85+
assert.equal(secondResult.patched, false)
86+
assert.equal(secondResult.source, firstResult.source)
87+
})
88+
6589
test('patches the installed workshop-app build', async () => {
6690
const result = await patchWorkshopApp()
6791

6892
assert.equal(typeof result.patched, 'boolean')
69-
assert.match(result.source, /action: action\$splatNotFound/)
70-
assert.match(result.source, /"routes\/\$": .*"hasAction": true/)
93+
assert.match(result.serverBuild.source, /action: action\$splatNotFound/)
94+
assert.match(result.serverBuild.source, /"routes\/\$": .*"hasAction": true/)
95+
assert.match(result.serverRuntime.source, /\^\\\/\{2,\}\$/)
7196
})

0 commit comments

Comments
 (0)