Skip to content

Commit 72d82ee

Browse files
authored
Merge pull request #603 from AikidoSec/allowed-ips-exact-match
Allow access to route if exact match does
2 parents d14e921 + e393fc7 commit 72d82ee

File tree

5 files changed

+154
-8
lines changed

5 files changed

+154
-8
lines changed

end2end/tests/hono-xml-allowlists.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ t.beforeEach(async () => {
3232
enabled: false,
3333
},
3434
},
35+
{
36+
route: "/admin/*",
37+
method: "GET",
38+
forceProtectionOff: false,
39+
allowedIPAddresses: ["10.0.0.1/16"],
40+
rateLimiting: {
41+
enabled: false,
42+
},
43+
},
44+
{
45+
route: "/admin/public",
46+
method: "GET",
47+
forceProtectionOff: false,
48+
allowedIPAddresses: ["0.0.0.0/0", "::/0"],
49+
rateLimiting: {
50+
enabled: false,
51+
},
52+
},
3553
],
3654
}),
3755
});
@@ -150,6 +168,22 @@ t.test("it blocks non-allowed IP addresses", (t) => {
150168
signal: AbortSignal.timeout(5000),
151169
});
152170
t.same(resp6.status, 403);
171+
172+
const resp7 = await fetch("http://127.0.0.1:4002/admin/public", {
173+
headers: {
174+
"X-Forwarded-For": "5.6.7.8",
175+
},
176+
signal: AbortSignal.timeout(5000),
177+
});
178+
t.same(resp7.status, 200);
179+
180+
const resp8 = await fetch("http://127.0.0.1:4002/admin/private", {
181+
headers: {
182+
"X-Forwarded-For": "5.6.7.8",
183+
},
184+
signal: AbortSignal.timeout(5000),
185+
});
186+
t.same(resp8.status, 403);
153187
})
154188
.catch((error) => {
155189
t.fail(error);

library/helpers/ip-matcher/IPMatcher.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,15 @@ t.test("Different cidr ranges", async (t) => {
192192
t.same(new IPMatcher(["123.2.0.2/31"]).has("123.2.0.1"), false);
193193
t.same(new IPMatcher(["123.2.0.2/32"]).has("123.2.0.2"), true);
194194
});
195+
196+
t.test("allow all ips", async (t) => {
197+
const matcher = new IPMatcher(["0.0.0.0/0", "::/0"]);
198+
t.same(matcher.has("1.2.3.4"), true);
199+
t.same(matcher.has("::1"), true);
200+
t.same(matcher.has("::ffff:1234"), true);
201+
t.same(matcher.has("1.1.1.1"), true);
202+
t.same(matcher.has("2002:db8::1"), true);
203+
t.same(matcher.has("10.0.0.1"), true);
204+
t.same(matcher.has("10.0.0.255"), true);
205+
t.same(matcher.has("192.168.1.1"), true);
206+
});

library/sources/http-server/ipAllowedToAccessRoute.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,31 @@ t.beforeEach(async () => {
3939
allowedIPAddresses: ["1.2.3.4", "192.168.2.0/24"],
4040
forceProtectionOff: false,
4141
},
42+
43+
{
44+
route: "/private/public",
45+
// @ts-expect-error Test
46+
rateLimiting: undefined,
47+
method: "GET",
48+
allowedIPAddresses: ["0.0.0.0/0", "::/0"],
49+
forceProtectionOff: false,
50+
},
51+
{
52+
route: "/private/public/*",
53+
// @ts-expect-error Test
54+
rateLimiting: undefined,
55+
method: "GET",
56+
allowedIPAddresses: ["0.0.0.0/0", "::/0"],
57+
forceProtectionOff: false,
58+
},
59+
{
60+
route: "/private/*",
61+
// @ts-expect-error Test
62+
rateLimiting: undefined,
63+
method: "GET",
64+
allowedIPAddresses: ["127.0.0.1", "::1"],
65+
forceProtectionOff: false,
66+
},
4267
],
4368
block: true,
4469
}),
@@ -88,6 +113,50 @@ t.test("it blocks request if no IP address", async () => {
88113
);
89114
});
90115

116+
t.test("public subroute of private route", async () => {
117+
t.same(
118+
ipAllowedToAccessRoute(
119+
{
120+
...context,
121+
url: "/private/test",
122+
route: "/private/test",
123+
method: "GET",
124+
remoteAddress: "1.1.1.1",
125+
},
126+
agent
127+
),
128+
false
129+
);
130+
t.same(
131+
ipAllowedToAccessRoute(
132+
{
133+
...context,
134+
url: "/private/public",
135+
route: "/private/public",
136+
method: "GET",
137+
remoteAddress: "1.1.1.1",
138+
},
139+
agent
140+
),
141+
true
142+
);
143+
144+
// No exact match and not all matching endpoints allow the IP address
145+
t.same(
146+
ipAllowedToAccessRoute(
147+
{
148+
...context,
149+
url: "/private/public/test",
150+
route: "/private/public/test",
151+
method: "GET",
152+
remoteAddress: "1.1.1.1",
153+
},
154+
agent
155+
),
156+
false
157+
);
158+
});
159+
91160
t.test("it allows request if configuration is broken", async () => {
92161
const agent = createTestAgent({
93162
token: new Token("123"),
@@ -203,7 +272,7 @@ t.test("it checks every matching endpoint", async () => {
203272

204273
t.same(
205274
ipAllowedToAccessRoute({ ...context, remoteAddress: "3.4.5.6" }, agent),
206-
false
275+
true // Because exact match allows the IP address
207276
);
208277
});
209278

library/sources/http-server/ipAllowedToAccessRoute.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,44 @@
11
import { Agent } from "../../agent/Agent";
22
import { Context } from "../../agent/Context";
33
import { isLocalhostIP } from "../../helpers/isLocalhostIP";
4+
import type { Endpoint } from "../../agent/Config";
5+
import type { IPMatcher } from "../../helpers/ip-matcher/IPMatcher";
46

57
export function ipAllowedToAccessRoute(context: Context, agent: Agent) {
8+
// Always allow localhost IPs
69
if (context.remoteAddress && isLocalhostIP(context.remoteAddress)) {
710
return true;
811
}
912

10-
const matches = agent.getConfig().getEndpoints(context);
13+
// Get all matching endpoints with allowedIPAddresses defined
14+
const matches = agent
15+
.getConfig()
16+
.getEndpoints(context)
17+
.filter(
18+
(m): m is Endpoint & { allowedIPAddresses: IPMatcher } =>
19+
m.allowedIPAddresses !== undefined
20+
);
1121

12-
for (const endpoint of matches) {
13-
if (!endpoint.allowedIPAddresses) {
14-
continue;
15-
}
22+
if (!matches.length) {
23+
// No matches found, so we can allow access
24+
return true;
25+
}
1626

17-
if (!context.remoteAddress) {
18-
return false;
27+
if (!context.remoteAddress) {
28+
// Always block if remote address is unknown
29+
return false;
30+
}
31+
32+
// Check exact match first
33+
// If exact match allows the IP address, we can allow access without checking other matching endpoint configurations
34+
const exact = matches.find((m) => m.route === context.route);
35+
if (exact && exact.allowedIPAddresses) {
36+
if (exact.allowedIPAddresses.has(context.remoteAddress)) {
37+
return true;
1938
}
39+
}
2040

41+
for (const endpoint of matches) {
2142
const { allowedIPAddresses } = endpoint;
2243

2344
if (!allowedIPAddresses.has(context.remoteAddress)) {

sample-apps/hono-xml/app.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ async function main() {
110110
);
111111
});
112112

113+
app.get("/admin/public", async (c) => {
114+
return c.html(
115+
`<html lang="en">
116+
<body>
117+
<h1>Public subpage of the admin panel</h1>
118+
</body>
119+
</html>`
120+
);
121+
});
122+
113123
app.post("/add-fast", async (c) => {
114124
const body = await c.req.text();
115125

0 commit comments

Comments
 (0)