71 lines
3.2 KiB
TypeScript
71 lines
3.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { BlockedUrlError, assertPublicUrl, isPrivateAddress } from "@/lib/clean-pdf/ssrf";
|
|
|
|
describe("isPrivateAddress", () => {
|
|
const privateIps = [
|
|
"127.0.0.1", "0.0.0.0", "10.1.2.3", "100.64.0.1", "169.254.169.254",
|
|
"172.16.0.1", "172.31.255.255", "192.168.1.1", "192.0.0.8", "198.18.0.1",
|
|
"224.0.0.1", "255.255.255.255",
|
|
"::1", "::", "fc00::1", "fd12:3456::1", "fe80::1", "::ffff:10.0.0.1", "::ffff:127.0.0.1",
|
|
"::ffff:7f00:1", "::ffff:a00:1", "::ffff:c0a8:101", "::ffff:a9fe:a9fe",
|
|
];
|
|
const publicIps = ["93.184.216.34", "8.8.8.8", "172.32.0.1", "2606:2800:220:1::1", "::ffff:8.8.8.8", "::ffff:808:808"];
|
|
|
|
it.each(privateIps)("блокирует %s", (ip) => expect(isPrivateAddress(ip)).toBe(true));
|
|
it.each(publicIps)("пропускает %s", (ip) => expect(isPrivateAddress(ip)).toBe(false));
|
|
});
|
|
|
|
describe("assertPublicUrl", () => {
|
|
const publicResolve = async () => [{ address: "93.184.216.34", family: 4 }];
|
|
const privateResolve = async () => [{ address: "172.18.0.2", family: 4 }];
|
|
const mixedResolve = async () => [
|
|
{ address: "93.184.216.34", family: 4 },
|
|
{ address: "10.0.0.5", family: 4 },
|
|
];
|
|
|
|
it("пропускает публичный https-URL", async () => {
|
|
const url = await assertPublicUrl("https://example.com/article", publicResolve);
|
|
expect(url.hostname).toBe("example.com");
|
|
});
|
|
|
|
it.each([
|
|
"file:///etc/passwd",
|
|
"ftp://example.com/x",
|
|
"chrome://settings",
|
|
"not a url",
|
|
])("блокирует %s", async (raw) => {
|
|
await expect(assertPublicUrl(raw, publicResolve)).rejects.toThrow(BlockedUrlError);
|
|
});
|
|
|
|
it("блокирует localhost и .local без резолва", async () => {
|
|
await expect(assertPublicUrl("http://localhost:3000/x", publicResolve)).rejects.toThrow(BlockedUrlError);
|
|
await expect(assertPublicUrl("http://printer.local/x", publicResolve)).rejects.toThrow(BlockedUrlError);
|
|
});
|
|
|
|
it("блокирует литеральный приватный IP", async () => {
|
|
await expect(assertPublicUrl("http://192.168.1.1/admin", publicResolve)).rejects.toThrow(BlockedUrlError);
|
|
await expect(assertPublicUrl("http://[::1]:8080/", publicResolve)).rejects.toThrow(BlockedUrlError);
|
|
});
|
|
|
|
it("блокирует хост, резолвящийся в приватный IP (включая частично)", async () => {
|
|
await expect(assertPublicUrl("https://evil.example/x", privateResolve)).rejects.toThrow(BlockedUrlError);
|
|
await expect(assertPublicUrl("https://evil.example/x", mixedResolve)).rejects.toThrow(BlockedUrlError);
|
|
});
|
|
|
|
it("блокирует хост, который не резолвится", async () => {
|
|
const failResolve = async () => { throw new Error("ENOTFOUND"); };
|
|
await expect(assertPublicUrl("https://nope.example/x", failResolve)).rejects.toThrow(BlockedUrlError);
|
|
});
|
|
|
|
it("блокирует v4-mapped IPv6 в bracket-нотации (SSRF-обход)", async () => {
|
|
for (const raw of [
|
|
"http://[::ffff:127.0.0.1]/",
|
|
"http://[::ffff:10.0.0.1]/",
|
|
"http://[::ffff:169.254.169.254]/",
|
|
"http://[::ffff:192.168.1.1]/",
|
|
]) {
|
|
await expect(assertPublicUrl(raw, publicResolve)).rejects.toThrow(BlockedUrlError);
|
|
}
|
|
});
|
|
});
|