Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/qa-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
cp firewall-node/.github/workflows/Dockerfile.qa zen-demo-nodejs/Dockerfile

- name: Run Firewall QA Tests
uses: AikidoSec/firewall-tester-action@releases/v1
uses: AikidoSec/firewall-tester-action@v1.0.4
with:
dockerfile_path: ./zen-demo-nodejs/Dockerfile
app_port: 3000
Expand Down
7 changes: 2 additions & 5 deletions end2end/tests/hono-xml-blocklists.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,8 @@ t.test("it does not block bypass IP if in blocklist", (t) => {
"X-Forwarded-For": "1.3.2.2",
},
});
t.same(resp3.status, 403);
t.same(
await resp3.text(),
`Your IP address is not allowed to access this resource. (Your IP: 1.3.2.2)`
);
t.same(resp3.status, 200);
t.match(await resp3.text(), "Admin panel");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So https://github.com/AikidoSec/zen-specs/pull/7/changes is obsolete? I still think it is confusing 😅

})
.catch((error) => {
t.fail(error);
Expand Down
5 changes: 4 additions & 1 deletion library/agent/ServiceConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isPrivateIP } from "../vulnerabilities/ssrf/isPrivateIP";
import type { Endpoint, EndpointConfig, Domain } from "./Config";
import type { IPList, UserAgentDetails } from "./api/FetchListsAPI";
import { safeCreateRegExp } from "./safeCreateRegExp";
import { addIPv4MappedAddresses } from "../helpers/addIPv4MappedAddresses";

export class ServiceConfig {
private blockedUserIds: Map<string, string> = new Map();
Expand Down Expand Up @@ -99,7 +100,9 @@ export class ServiceConfig {
this.bypassedIPAddresses = undefined;
return;
}
this.bypassedIPAddresses = new IPMatcher(ipAddresses);
this.bypassedIPAddresses = new IPMatcher(
addIPv4MappedAddresses(ipAddresses)
);
}

isBypassedIP(ip: string) {
Expand Down
20 changes: 11 additions & 9 deletions library/agent/hooks/wrapExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Agent } from "../Agent";
import { getInstance } from "../AgentSingleton";
import { OperationKind } from "../api/Event";
import { bindContext, getContext } from "../Context";
import type { InterceptorResult } from "./InterceptorResult";
import { type InterceptorResult, isAttackResult } from "./InterceptorResult";
import type { PartialWrapPackageInfo } from "./WrapPackageInfo";
import { wrapDefaultOrNamed } from "./wrapDefaultOrNamed";
import { onInspectionInterceptorResult } from "./onInspectionInterceptorResult";
Expand Down Expand Up @@ -151,14 +151,6 @@ export function inspectArgs(
methodName: string,
kind: OperationKind | undefined
) {
if (context) {
const matches = agent.getConfig().getEndpoints(context);

if (matches.find((match) => match.forceProtectionOff)) {
return;
}
}

const start = performance.now();
let result: InterceptorResult = undefined;

Expand All @@ -177,6 +169,16 @@ export function inspectArgs(
});
}

// When forceProtectionOff is enabled, skip attack detection
// but still allow outbound connection blocking
if (context && isAttackResult(result)) {
const matches = agent.getConfig().getEndpoints(context);

if (matches.find((match) => match.forceProtectionOff)) {
return;
}
}

onInspectionInterceptorResult(
context,
agent,
Expand Down
21 changes: 21 additions & 0 deletions library/helpers/addIPv4MappedAddresses.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as t from "tap";
import { addIPv4MappedAddresses } from "./addIPv4MappedAddresses";

t.test("it adds IPv4-mapped IPv6 addresses", async (t) => {
t.same(
addIPv4MappedAddresses([
"1.2.3.4",
"23.45.67.89/24",
"2606:2800:220:1:248:1893:25c8:1946",
"2001:0db9:abcd:1234::/64",
]),
[
"1.2.3.4",
"23.45.67.89/24",
"2606:2800:220:1:248:1893:25c8:1946",
"2001:0db9:abcd:1234::/64",
"::ffff:1.2.3.4/128",
"::ffff:23.45.67.89/120",
]
);
});
10 changes: 10 additions & 0 deletions library/helpers/addIPv4MappedAddresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import mapIPv4ToIPv6 from "./mapIPv4ToIPv6";

/**
* Adds IPv4-mapped IPv6 versions for all IPv4 addresses in the array.
* e.g. ["1.2.3.4", "2001:db8::/32"] -> ["1.2.3.4", "2001:db8::/32", "::ffff:1.2.3.4/128"]
*/
export function addIPv4MappedAddresses(ips: string[]): string[] {
const ipv4Addresses = ips.filter((ip) => !ip.includes(":"));
return ips.concat(ipv4Addresses.map(mapIPv4ToIPv6));
}
8 changes: 8 additions & 0 deletions library/middleware/shouldBlockRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export function shouldBlockRequest(): Result {
updateContext(context, "executedMiddleware", true);
agent.onMiddlewareExecuted();

const isBypassedIP =
context.remoteAddress &&
agent.getConfig().isBypassedIP(context.remoteAddress);

if (isBypassedIP) {
return { block: false };
}

if (context.user && agent.getConfig().isUserBlocked(context.user.id)) {
return { block: true, type: "blocked", trigger: "user" };
}
Expand Down
65 changes: 0 additions & 65 deletions library/ratelimiting/shouldRateLimitRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,37 +182,6 @@ t.test("it rate limits localhost when not in production mode", async (t) => {
});
});

t.test("it does not rate limit when the IP is allowed", async (t) => {
const agent = await createAgent(
[
{
method: "POST",
route: "/login",
forceProtectionOff: false,
rateLimiting: {
enabled: true,
maxRequests: 3,
windowSizeInMS: 1000,
},
},
],
["1.2.3.4"]
);

t.same(shouldRateLimitRequest(createContext("1.2.3.4"), agent), {
block: false,
});
t.same(shouldRateLimitRequest(createContext("1.2.3.4"), agent), {
block: false,
});
t.same(shouldRateLimitRequest(createContext("1.2.3.4"), agent), {
block: false,
});
t.same(shouldRateLimitRequest(createContext("1.2.3.4"), agent), {
block: false,
});
});

t.test("it rate limits by user", async (t) => {
const agent = await createAgent([
{
Expand Down Expand Up @@ -437,40 +406,6 @@ t.test(
}
);

t.test(
"it does not rate limit requests from allowed ip with user",
async (t) => {
const agent = await createAgent(
[
{
method: "POST",
route: "/login",
forceProtectionOff: false,
rateLimiting: {
enabled: true,
maxRequests: 3,
windowSizeInMS: 1000,
},
},
],
["1.2.3.4"]
);

t.same(shouldRateLimitRequest(createContext("1.2.3.4", "123"), agent), {
block: false,
});
t.same(shouldRateLimitRequest(createContext("1.2.3.4", "123"), agent), {
block: false,
});
t.same(shouldRateLimitRequest(createContext("1.2.3.4", "123"), agent), {
block: false,
});
t.same(shouldRateLimitRequest(createContext("1.2.3.4", "123"), agent), {
block: false,
});
}
);

t.test(
"it does not consume rate limit for user a second time (same request)",
async (t) => {
Expand Down
7 changes: 1 addition & 6 deletions library/ratelimiting/shouldRateLimitRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,7 @@ export function shouldRateLimitRequest(
isLocalhostIP(context.remoteAddress) &&
isProduction;

// Allow requests from allowed IPs, e.g. never rate limit office IPs
const isBypassedIP =
context.remoteAddress &&
agent.getConfig().isBypassedIP(context.remoteAddress);

if (isFromLocalhostInProduction || isBypassedIP) {
if (isFromLocalhostInProduction) {
return { block: false };
}

Expand Down
16 changes: 8 additions & 8 deletions library/sources/http-server/checkIfRequestIsBlocked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export function checkIfRequestIsBlocked(
// Also ensures that the statistics are only counted once
res[checkedBlocks] = true;

const isBypassedIP =
context.remoteAddress &&
agent.getConfig().isBypassedIP(context.remoteAddress);

if (isBypassedIP) {
return false;
}

if (!ipAllowedToAccessRoute(context, agent)) {
res.statusCode = 403;
res.setHeader("Content-Type", "text/plain");
Expand All @@ -54,14 +62,6 @@ export function checkIfRequestIsBlocked(
return true;
}

const isBypassedIP =
context.remoteAddress &&
agent.getConfig().isBypassedIP(context.remoteAddress);

if (isBypassedIP) {
return false;
}

if (
context.remoteAddress &&
!agent.getConfig().isAllowedIPAddress(context.remoteAddress).allowed
Expand Down
22 changes: 11 additions & 11 deletions library/vulnerabilities/ssrf/inspectDNSLookupCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@ function wrapDNSLookupCallback(
}
}

const isBypassedIP =
context &&
context.remoteAddress &&
agent.getConfig().isBypassedIP(context.remoteAddress);

if (isBypassedIP) {
// If the IP address is allowed, we don't need to block the request
// Just call the original callback to allow the DNS lookup
return callback(err, addresses, family);
}

if (!found) {
if (imdsIpResult.isIMDS) {
// Stored SSRF attack executed during another request (context set)
Expand Down Expand Up @@ -211,17 +222,6 @@ function wrapDNSLookupCallback(
return callback(err, addresses, family);
}

const isBypassedIP =
context &&
context.remoteAddress &&
agent.getConfig().isBypassedIP(context.remoteAddress);

if (isBypassedIP) {
// If the IP address is allowed, we don't need to block the request
// Just call the original callback to allow the DNS lookup
return callback(err, addresses, family);
}

// Used to get the stack trace of the calling location
// We don't throw the error, we just use it to get the stack trace
const stackTraceError = callingLocationStackTrace || new Error();
Expand Down
Loading