Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -51,7 +51,7 @@ jobs:
cp firewall-node/.github/workflows/Dockerfile.qa zen-demo-nodejs/Dockerfile

- name: Run Firewall QA Tests
uses: AikidoSec/firewall-tester-action@d6ce69ab0d6b52cac12d01be9b25603de492bfc5 # v1 branch
uses: AikidoSec/firewall-tester-action@af36cba78b7a99542b3f1e011c30322a7d47ad7a # v1.0.16
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 @@ -316,11 +316,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");

// IPv4-mapped IPv6 address should also bypass (matches bypass list 1.3.2.1)
const resp4 = await fetch("http://127.0.0.1:4004/", {
Expand Down
3 changes: 2 additions & 1 deletion library/agent/Hostnames.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { hostnameToUnicode } from "../helpers/hostnameToUnicode";
import { normalizeHostname } from "../helpers/normalizeHostname";

type Ports = Map<number, number>;
Expand All @@ -12,7 +13,7 @@ export class Hostnames {
return;
}

hostname = normalizeHostname(hostname);
hostname = hostnameToUnicode(normalizeHostname(hostname));
Comment thread
timokoessler marked this conversation as resolved.

if (!this.map.has(hostname)) {
this.map.set(hostname, new Map([[port, 1]]));
Expand Down
5 changes: 4 additions & 1 deletion library/agent/ServiceConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { addIPv4MappedAddresses } from "../helpers/addIPv4MappedAddresses";
import { hostnameToUnicode } from "../helpers/hostnameToUnicode";
import { IPMatcher } from "../helpers/ip-matcher/IPMatcher";
import { LimitedContext, matchEndpoints } from "../helpers/matchEndpoints";
import { normalizeHostname } from "../helpers/normalizeHostname";
Expand Down Expand Up @@ -297,7 +298,9 @@ export class ServiceConfig {
}

shouldBlockOutgoingRequest(hostname: string): boolean {
const mode = this.domains.get(normalizeHostname(hostname));
const mode = this.domains.get(
hostnameToUnicode(normalizeHostname(hostname))
);

if (this.blockNewOutgoingRequests) {
// Only allow outgoing requests if the mode is "allow"
Expand Down
20 changes: 12 additions & 8 deletions library/agent/hooks/onInspectionInterceptorResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,39 @@ export function onInspectionInterceptorResult(
) {
const end = performance.now();

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

if (kind) {
agent.getInspectionStatistics().onInspectedCall({
operation: operation,
kind: kind,
attackDetected: !!result,
attackDetected: !isBypassedIP && !!result,
blocked: agent.shouldBlock(),
durationInMs: end - start,
withoutContext: !context,
});
}

const isBypassedIP =
context &&
context.remoteAddress &&
agent.getConfig().isBypassedIP(context.remoteAddress);
if (isBypassedIP) {
return;
}

if (isIdorViolationResult(result) && !isBypassedIP) {
if (isIdorViolationResult(result)) {
throw cleanError(new Error(result.message));
}

if (isBlockOutboundConnectionResult(result) && !isBypassedIP) {
if (isBlockOutboundConnectionResult(result)) {
throw cleanError(
new Error(
`Zen has blocked an outbound connection: ${result.operation}(...) to ${escapeHTML(result.hostname)}`
)
);
}

if (isAttackResult(result) && context && !isBypassedIP) {
if (isAttackResult(result) && context) {
// Flag request as having an attack detected
updateContext(context, "attackDetected", true);

Expand Down
20 changes: 20 additions & 0 deletions library/helpers/hostnameToUnicode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { domainToUnicode } from "url";

/**
* Converts a punycode hostname to its unicode form.
* e.g. "xn--mnchen-3ya.example.com" -> "münchen.example.com"
*
* Returns the original hostname if conversion fails or produces an empty result.
*/
export function hostnameToUnicode(hostname: string): string {
try {
const unicode = domainToUnicode(hostname);
if (unicode) {
return unicode;
}
} catch {
// Ignore
Comment thread
hansott marked this conversation as resolved.
}

return hostname;
}
8 changes: 8 additions & 0 deletions library/middleware/shouldBlockRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,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 @@ -184,37 +184,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 @@ -439,40 +408,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 @@ -55,12 +55,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/blockIPsAndBots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ export function blockIPsAndBots(
// 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 @@ -53,14 +61,6 @@ export function blockIPsAndBots(
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 @@ -186,6 +186,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 @@ -214,17 +225,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