Skip to content

Commit 64b7749

Browse files
Merge pull request #233 from AikidoSec/patch-url
Only discover endpoints/routes that exist
2 parents 35705b3 + 5f04ab4 commit 64b7749

File tree

6 files changed

+236
-21
lines changed

6 files changed

+236
-21
lines changed

library/agent/Agent.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,11 +512,9 @@ t.test("it sends hostnames and routes along with heartbeat", async () => {
512512
agent.onConnectHostname("aikido.dev", 80);
513513
agent.onConnectHostname("google.com", 443);
514514
agent.onRouteExecute("POST", "/posts/:id");
515-
agent.onRouteExecute("OPTIONS", "/posts/:id");
516515
agent.onRouteExecute("POST", "/posts/:id");
517516
agent.onRouteExecute("GET", "/posts/:id");
518517
agent.onRouteExecute("GET", "/");
519-
agent.onRouteExecute("HEAD", "/");
520518

521519
api.clear();
522520

library/agent/Agent.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,6 @@ export class Agent {
412412
}
413413

414414
onRouteExecute(method: string, path: string) {
415-
const excludedMethods = ["OPTIONS", "HEAD"];
416-
417-
if (excludedMethods.includes(method)) {
418-
return;
419-
}
420-
421415
this.routes.addRoute(method, path);
422416
}
423417

library/sources/HTTPServer.test.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,9 @@ t.test("it discovers routes", async () => {
171171
});
172172

173173
await new Promise<void>((resolve) => {
174-
server.listen(3318, () => {
174+
server.listen(3340, () => {
175175
fetch({
176-
url: new URL("http://localhost:3318/foo/bar"),
176+
url: new URL("http://localhost:3340/foo/bar"),
177177
method: "GET",
178178
headers: {},
179179
timeoutInMS: 500,
@@ -195,6 +195,38 @@ t.test("it discovers routes", async () => {
195195
});
196196
});
197197

198+
t.test(
199+
"it does not discover route if server response is error code",
200+
async () => {
201+
const http = require("http");
202+
const server = http.createServer((req, res) => {
203+
res.statusCode = 404;
204+
res.end();
205+
});
206+
207+
await new Promise<void>((resolve) => {
208+
server.listen(3341, () => {
209+
fetch({
210+
url: new URL("http://localhost:3341/not-found"),
211+
method: "GET",
212+
headers: {},
213+
timeoutInMS: 500,
214+
}).then(() => {
215+
t.equal(
216+
agent
217+
.getRoutes()
218+
.asArray()
219+
.find((route) => route.path === "/not-found"),
220+
undefined
221+
);
222+
server.close();
223+
resolve();
224+
});
225+
});
226+
});
227+
}
228+
);
229+
198230
t.test("it parses cookies", async () => {
199231
const http = require("http");
200232
const server = http.createServer((req, res) => {

library/sources/http-server/createRequestListener.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
import type {
2-
IncomingMessage,
3-
OutgoingMessage,
4-
RequestListener,
5-
ServerResponse,
6-
} from "http";
1+
import type { IncomingMessage, RequestListener, ServerResponse } from "http";
72
import { Agent } from "../../agent/Agent";
83
import { getContext, runWithContext } from "../../agent/Context";
9-
import { buildRouteFromURL } from "../../helpers/buildRouteFromURL";
104
import { escapeHTML } from "../../helpers/escapeHTML";
115
import { shouldRateLimitRequest } from "../../ratelimiting/shouldRateLimitRequest";
126
import { contextFromRequest } from "./contextFromRequest";
137
import { readBodyStream } from "./readBodyStream";
8+
import { shouldDiscoverRoute } from "./shouldDiscoverRoute";
149

1510
export function createRequestListener(
1611
listener: Function,
@@ -50,13 +45,23 @@ function callListenerWithContext(
5045
) {
5146
const context = contextFromRequest(req, body, module);
5247

53-
if (context.route && context.method) {
54-
agent.onRouteExecute(context.method, context.route);
55-
}
56-
5748
return runWithContext(context, () => {
5849
res.on("finish", () => {
5950
const context = getContext();
51+
52+
if (
53+
context &&
54+
context.route &&
55+
context.method &&
56+
shouldDiscoverRoute({
57+
statusCode: res.statusCode,
58+
route: context.route,
59+
method: context.method,
60+
})
61+
) {
62+
agent.onRouteExecute(context.method, context.route);
63+
}
64+
6065
agent.getInspectionStatistics().onRequest();
6166
if (context && context.attackDetected) {
6267
agent.getInspectionStatistics().onDetectedAttack({
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import * as t from "tap";
2+
import { shouldDiscoverRoute } from "./shouldDiscoverRoute";
3+
4+
t.test(
5+
"it does not discover route if not found or method not allowed",
6+
async () => {
7+
t.same(
8+
shouldDiscoverRoute({ statusCode: 404, route: "/", method: "GET" }),
9+
false
10+
);
11+
t.same(
12+
shouldDiscoverRoute({ statusCode: 405, route: "/", method: "GET" }),
13+
false
14+
);
15+
}
16+
);
17+
18+
t.test("it discovers route for all other status codes", async () => {
19+
t.same(
20+
shouldDiscoverRoute({ statusCode: 200, route: "/", method: "GET" }),
21+
true
22+
);
23+
t.same(
24+
shouldDiscoverRoute({ statusCode: 500, route: "/", method: "GET" }),
25+
true
26+
);
27+
t.same(
28+
shouldDiscoverRoute({ statusCode: 400, route: "/", method: "GET" }),
29+
true
30+
);
31+
t.same(
32+
shouldDiscoverRoute({ statusCode: 300, route: "/", method: "GET" }),
33+
true
34+
);
35+
t.same(
36+
shouldDiscoverRoute({ statusCode: 201, route: "/", method: "GET" }),
37+
true
38+
);
39+
});
40+
41+
t.test("it does not discover route for OPTIONS or HEAD methods", async () => {
42+
t.same(
43+
shouldDiscoverRoute({ statusCode: 200, route: "/", method: "OPTIONS" }),
44+
false
45+
);
46+
t.same(
47+
shouldDiscoverRoute({ statusCode: 200, route: "/", method: "HEAD" }),
48+
false
49+
);
50+
});
51+
52+
t.test(
53+
"it does not discover route for OPTIONS or HEAD methods even with other status codes",
54+
async () => {
55+
t.same(
56+
shouldDiscoverRoute({ statusCode: 404, route: "/", method: "OPTIONS" }),
57+
false
58+
);
59+
t.same(
60+
shouldDiscoverRoute({ statusCode: 405, route: "/", method: "HEAD" }),
61+
false
62+
);
63+
}
64+
);
65+
66+
t.test("it does not discover static files", async () => {
67+
t.same(
68+
shouldDiscoverRoute({
69+
statusCode: 200,
70+
route: "/service-worker.js",
71+
method: "GET",
72+
}),
73+
false
74+
);
75+
t.same(
76+
shouldDiscoverRoute({
77+
statusCode: 200,
78+
route: "/precache-manifest.10faec0bee24db502c8498078126dd53.js",
79+
method: "POST",
80+
}),
81+
false
82+
);
83+
t.same(
84+
shouldDiscoverRoute({
85+
statusCode: 200,
86+
route: "/img/icons/favicon-16x16.png",
87+
method: "GET",
88+
}),
89+
false
90+
);
91+
t.same(
92+
shouldDiscoverRoute({
93+
statusCode: 200,
94+
route: "/fonts/icomoon.ttf",
95+
method: "GET",
96+
}),
97+
false
98+
);
99+
});
100+
101+
t.test("it allows html files", async () => {
102+
t.same(
103+
shouldDiscoverRoute({
104+
statusCode: 200,
105+
route: "/index.html",
106+
method: "GET",
107+
}),
108+
false
109+
);
110+
t.same(
111+
shouldDiscoverRoute({
112+
statusCode: 200,
113+
route: "/contact.html",
114+
method: "GET",
115+
}),
116+
false
117+
);
118+
});
119+
120+
t.test("it allows files with extension of one character", async () => {
121+
t.same(
122+
shouldDiscoverRoute({
123+
statusCode: 200,
124+
route: "/a.a",
125+
method: "GET",
126+
}),
127+
true
128+
);
129+
});
130+
131+
t.test("it allows files with extension of 5 or more characters", async () => {
132+
t.same(
133+
shouldDiscoverRoute({
134+
statusCode: 200,
135+
route: "/a.aaaaa",
136+
method: "GET",
137+
}),
138+
true
139+
);
140+
t.same(
141+
shouldDiscoverRoute({
142+
statusCode: 200,
143+
route: "/a.aaaaaa",
144+
method: "GET",
145+
}),
146+
true
147+
);
148+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { extname } from "path";
2+
3+
const NOT_FOUND = 404;
4+
const METHOD_NOT_ALLOWED = 405;
5+
const ERROR_CODES = [NOT_FOUND, METHOD_NOT_ALLOWED];
6+
7+
export function shouldDiscoverRoute({
8+
statusCode,
9+
route,
10+
method,
11+
}: {
12+
statusCode: number;
13+
route: string;
14+
method: string;
15+
}) {
16+
const excludedMethods = ["OPTIONS", "HEAD"];
17+
18+
if (excludedMethods.includes(method)) {
19+
return false;
20+
}
21+
22+
if (ERROR_CODES.includes(statusCode)) {
23+
return false;
24+
}
25+
26+
let extension = extname(route);
27+
28+
if (extension && extension.startsWith(".")) {
29+
// Remove the dot from the extension
30+
extension = extension.slice(1);
31+
32+
if (extension.length >= 2 && extension.length <= 4) {
33+
return false;
34+
}
35+
}
36+
37+
return true;
38+
}

0 commit comments

Comments
 (0)