security: SSRF guard covers IPv6 + DNS-rebind defence via pinned IP (#49)
Expand the SSRF blocklist from IPv4-only to IPv6 loopback/ULA (fc00::/7)/
link-local (fe80::/10)/multicast/IPv4-mapped, plus the missing IPv4 ranges
0.0.0.0/8, 100.64.0.0/10 CGNAT, and TEST-NET/benchmark ranges. Replace the
single-lookup SSRF guard with resolveAndValidate(): resolves all DNS records
(lookup { all: true }) so a hostname returning "public + private" is
rejected, and returns the first validated address for connection pinning.
The webhook dispatcher now switches from plain fetch() to https.request()
with a custom Agent.lookup that returns the pre-validated IP. A DNS rebind
between the guard check and the TCP connect() can no longer redirect the
dial to an internal address. Hostname still flows through for SNI and
certificate validation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -716,19 +716,26 @@ describe("user profile and TOTP self-service", () => {
|
||||
totpEnabled: false,
|
||||
});
|
||||
const update = vi.fn().mockResolvedValue({});
|
||||
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
|
||||
const caller = createAdminCaller({
|
||||
user: {
|
||||
findUnique,
|
||||
update,
|
||||
updateMany,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await caller.verifyAndEnableTotp({ token: "123456" });
|
||||
|
||||
expect(result).toEqual({ enabled: true });
|
||||
// lastTotpAt is written atomically by updateMany (the replay guard);
|
||||
// user.update only toggles the enabled flag after the CAS succeeds.
|
||||
expect(updateMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ data: { lastTotpAt: expect.any(Date) } }),
|
||||
);
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: "user_admin" },
|
||||
data: { totpEnabled: true, lastTotpAt: expect.any(Date) },
|
||||
data: { totpEnabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -743,10 +750,12 @@ describe("user profile and TOTP self-service", () => {
|
||||
lastTotpAt: null,
|
||||
});
|
||||
const update = vi.fn().mockResolvedValue({});
|
||||
const updateMany = vi.fn().mockResolvedValue({ count: 1 });
|
||||
const caller = createAdminCaller({
|
||||
user: {
|
||||
findUnique,
|
||||
update,
|
||||
updateMany,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -757,10 +766,9 @@ describe("user profile and TOTP self-service", () => {
|
||||
where: { id: "user_admin" },
|
||||
select: { id: true, totpSecret: true, totpEnabled: true, lastTotpAt: true },
|
||||
});
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: "user_admin" },
|
||||
data: { lastTotpAt: expect.any(Date) },
|
||||
});
|
||||
expect(updateMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ data: { lastTotpAt: expect.any(Date) } }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects invalid login-flow TOTP tokens with UNAUTHORIZED", async () => {
|
||||
|
||||
Reference in New Issue
Block a user