-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #394 from AikidoSec/patch-host-header
Fix false positive for application doing a request to itself on localhost
- Loading branch information
Showing
11 changed files
with
683 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
/* eslint-disable prefer-rest-params */ | ||
import * as t from "tap"; | ||
import { Agent } from "../agent/Agent"; | ||
import { createServer, Server } from "http"; | ||
import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting"; | ||
import { Token } from "../agent/api/Token"; | ||
import { Context, runWithContext } from "../agent/Context"; | ||
import { LoggerNoop } from "../agent/logger/LoggerNoop"; | ||
import { Fetch } from "./Fetch"; | ||
|
||
function createContext({ | ||
url, | ||
hostHeader, | ||
body, | ||
additionalHeaders = {}, | ||
}: { | ||
url: string; | ||
hostHeader: string; | ||
body: unknown; | ||
additionalHeaders?: Record<string, string>; | ||
}): Context { | ||
return { | ||
url: url, | ||
method: "GET", | ||
headers: { | ||
host: hostHeader, | ||
connection: "keep-alive", | ||
"cache-control": "max-age=0", | ||
"sec-ch-ua": | ||
'"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"', | ||
"sec-ch-ua-mobile": "?0", | ||
"sec-ch-ua-platform": '"macOS"', | ||
"upgrade-insecure-requests": "1", | ||
"user-agent": | ||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", | ||
accept: | ||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", | ||
"sec-fetch-site": "none", | ||
"sec-fetch-mode": "navigate", | ||
"sec-fetch-user": "?1", | ||
"sec-fetch-dest": "document", | ||
"accept-encoding": "gzip, deflate, br, zstd", | ||
"accept-language": "nl,en;q=0.9,en-US;q=0.8", | ||
...additionalHeaders, | ||
}, | ||
route: "/", | ||
query: {}, | ||
source: "express", | ||
routeParams: {}, | ||
cookies: {}, | ||
remoteAddress: "127.0.0.1", | ||
subdomains: [], | ||
body: body, | ||
}; | ||
} | ||
|
||
const agent = new Agent( | ||
true, | ||
new LoggerNoop(), | ||
new ReportingAPIForTesting(), | ||
new Token("123"), | ||
undefined | ||
); | ||
|
||
agent.start([new Fetch()]); | ||
|
||
const port = 1341; | ||
const serverUrl = `http://localhost:${port}`; | ||
const hostHeader = `localhost:${port}`; | ||
|
||
let server: Server; | ||
t.before(async () => { | ||
server = createServer((_, res) => { | ||
res.writeHead(200, { "Content-Type": "text/plain" }); | ||
res.end("Hello World\n"); | ||
}); | ||
|
||
return new Promise<void>((resolve) => { | ||
server.listen(port, resolve); | ||
server.unref(); | ||
}); | ||
}); | ||
|
||
t.test( | ||
"it does not block request to localhost with same port", | ||
{ skip: !global.fetch ? "fetch is not available" : false }, | ||
async (t) => { | ||
await runWithContext( | ||
createContext({ | ||
url: serverUrl, | ||
hostHeader: hostHeader, | ||
body: {}, | ||
}), | ||
async () => { | ||
// Server doing a request to itself | ||
const response = await fetch(`${serverUrl}/favicon.ico`); | ||
// The server should respond with a 200 | ||
t.same(response.status, 200); | ||
} | ||
); | ||
} | ||
); | ||
|
||
t.test( | ||
"it does not block request to localhost with same port using the origin header", | ||
{ skip: !global.fetch ? "fetch is not available" : false }, | ||
async (t) => { | ||
await runWithContext( | ||
createContext({ | ||
url: serverUrl, | ||
hostHeader: "", | ||
body: {}, | ||
additionalHeaders: { | ||
origin: serverUrl, | ||
}, | ||
}), | ||
async () => { | ||
// Server doing a request to itself | ||
const response = await fetch(`${serverUrl}/favicon.ico`); | ||
// The server should respond with a 200 | ||
t.same(response.status, 200); | ||
} | ||
); | ||
} | ||
); | ||
|
||
t.test( | ||
"it does not block request to localhost with same port using the referer header", | ||
{ skip: !global.fetch ? "fetch is not available" : false }, | ||
async (t) => { | ||
await runWithContext( | ||
createContext({ | ||
url: serverUrl, | ||
hostHeader: "", | ||
body: {}, | ||
additionalHeaders: { | ||
referer: serverUrl, | ||
}, | ||
}), | ||
async () => { | ||
// Server doing a request to itself | ||
const response = await fetch(`${serverUrl}/favicon.ico`); | ||
// The server should respond with a 200 | ||
t.same(response.status, 200); | ||
} | ||
); | ||
} | ||
); | ||
|
||
t.test( | ||
"it blocks requests to other ports", | ||
{ skip: !global.fetch ? "fetch is not available" : false }, | ||
async (t) => { | ||
const error = await t.rejects(async () => { | ||
await runWithContext( | ||
createContext({ | ||
url: `http://localhost:${port + 1}`, | ||
hostHeader: `localhost:${port + 1}`, | ||
body: { | ||
url: `${serverUrl}/favicon.ico`, | ||
}, | ||
}), | ||
async () => { | ||
// Server doing a request to localhost but with a different port | ||
// This should be blocked | ||
await fetch(`${serverUrl}/favicon.ico`); | ||
// This should not be called | ||
t.fail(); | ||
} | ||
); | ||
}); | ||
|
||
t.ok(error instanceof Error); | ||
if (error instanceof Error) { | ||
t.same( | ||
error.message, | ||
"Zen has blocked a server-side request forgery: fetch(...) originating from body.url" | ||
); | ||
} | ||
} | ||
); | ||
|
||
t.after(() => { | ||
server.close(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
/* eslint-disable prefer-rest-params */ | ||
import * as t from "tap"; | ||
import { Agent } from "../agent/Agent"; | ||
import { createServer, IncomingMessage, Server } from "http"; | ||
import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting"; | ||
import { Token } from "../agent/api/Token"; | ||
import { Context, runWithContext } from "../agent/Context"; | ||
import { LoggerNoop } from "../agent/logger/LoggerNoop"; | ||
import { HTTPRequest } from "./HTTPRequest"; | ||
|
||
function createContext({ | ||
url, | ||
hostHeader, | ||
body, | ||
}: { | ||
url: string; | ||
hostHeader: string; | ||
body: unknown; | ||
}): Context { | ||
return { | ||
url: url, | ||
method: "GET", | ||
headers: { | ||
host: hostHeader, | ||
connection: "keep-alive", | ||
"cache-control": "max-age=0", | ||
"sec-ch-ua": | ||
'"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"', | ||
"sec-ch-ua-mobile": "?0", | ||
"sec-ch-ua-platform": '"macOS"', | ||
"upgrade-insecure-requests": "1", | ||
"user-agent": | ||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", | ||
accept: | ||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", | ||
"sec-fetch-site": "none", | ||
"sec-fetch-mode": "navigate", | ||
"sec-fetch-user": "?1", | ||
"sec-fetch-dest": "document", | ||
"accept-encoding": "gzip, deflate, br, zstd", | ||
"accept-language": "nl,en;q=0.9,en-US;q=0.8", | ||
}, | ||
route: "/", | ||
query: {}, | ||
source: "express", | ||
routeParams: {}, | ||
cookies: {}, | ||
remoteAddress: "127.0.0.1", | ||
subdomains: [], | ||
body: body, | ||
}; | ||
} | ||
|
||
const agent = new Agent( | ||
true, | ||
new LoggerNoop(), | ||
new ReportingAPIForTesting(), | ||
new Token("123"), | ||
undefined | ||
); | ||
|
||
agent.start([new HTTPRequest()]); | ||
|
||
const port = 1343; | ||
const serverUrl = `http://localhost:${port}`; | ||
const hostHeader = `localhost:${port}`; | ||
|
||
let server: Server; | ||
t.before(async () => { | ||
server = createServer((_, res) => { | ||
res.writeHead(200, { "Content-Type": "text/plain" }); | ||
res.end("Hello World\n"); | ||
}); | ||
|
||
return new Promise<void>((resolve) => { | ||
server.listen(port, resolve); | ||
server.unref(); | ||
}); | ||
}); | ||
|
||
t.test("it does not block request to localhost with same port", (t) => { | ||
const http = require("http"); | ||
|
||
runWithContext( | ||
createContext({ | ||
url: serverUrl, | ||
hostHeader: hostHeader, | ||
body: {}, | ||
}), | ||
() => { | ||
// Server doing a request to itself | ||
// Let's simulate a request to a favicon | ||
const request = http.request(`${serverUrl}/favicon.ico`); | ||
request.on("response", (response: IncomingMessage) => { | ||
// The server should respond with a 200 | ||
// Because we'll allow requests to localhost if it's the same port | ||
t.same(response.statusCode, 200); | ||
response.on("data", () => {}); | ||
response.on("end", () => {}); | ||
}); | ||
request.end(); | ||
} | ||
); | ||
|
||
const errors: Error[] = []; | ||
process.on("uncaughtException", (error) => { | ||
errors.push(error); | ||
}); | ||
|
||
setTimeout(() => { | ||
t.same(errors, []); | ||
t.end(); | ||
}, 1000); | ||
}); | ||
|
||
t.test("it blocks requests to other ports", (t) => { | ||
const http = require("http"); | ||
|
||
runWithContext( | ||
createContext({ | ||
url: `http://localhost:${port + 1}`, | ||
hostHeader: `localhost:${port + 1}`, | ||
body: { | ||
url: `${serverUrl}/favicon.ico`, | ||
}, | ||
}), | ||
() => { | ||
try { | ||
// Server doing a request to localhost but with a different port | ||
// This should be blocked | ||
const request = http.request(`${serverUrl}/favicon.ico`); | ||
request.on("response", (response: IncomingMessage) => { | ||
// This should not be called | ||
t.fail(); | ||
response.on("data", () => {}); | ||
response.on("end", () => {}); | ||
}); | ||
request.end(); | ||
} catch (error) { | ||
t.ok(error instanceof Error); | ||
if (error instanceof Error) { | ||
t.same( | ||
error.message, | ||
"Zen has blocked a server-side request forgery: http.request(...) originating from body.url" | ||
); | ||
} | ||
} | ||
} | ||
); | ||
|
||
setTimeout(() => { | ||
t.end(); | ||
}, 1000); | ||
}); | ||
|
||
t.after(() => { | ||
server.close(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.