@aikidosec/firewall
Version:
Zen by Aikido is an embedded Application Firewall that autonomously protects Node.js apps against common and critical attacks, provides rate limiting, detects malicious traffic (including bots), and more.
117 lines (116 loc) • 5.26 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRequestListener = createRequestListener;
const Context_1 = require("../../agent/Context");
const escapeHTML_1 = require("../../helpers/escapeHTML");
const isPackageInstalled_1 = require("../../helpers/isPackageInstalled");
const shouldBlockRequest_1 = require("../../middleware/shouldBlockRequest");
const blockIPsAndBots_1 = require("./blockIPsAndBots");
const contextFromRequest_1 = require("./contextFromRequest");
const readBodyStream_1 = require("./readBodyStream");
const shouldDiscoverRoute_1 = require("./shouldDiscoverRoute");
function createRequestListener(listener, module, agent) {
const isMicroInstalled = (0, isPackageInstalled_1.isPackageInstalled)("micro");
return async function requestListener(req, res) {
// Parse body only if next or micro is installed
// We can only read the body stream once
// This is tricky, see replaceRequestBody(...)
// e.g. Hono uses web requests and web streams
// (uses Readable.toWeb(req) to convert to a web stream)
const readBodyAndRateLimit = "NEXT_DEPLOYMENT_ID" in process.env || isMicroInstalled;
if (!readBodyAndRateLimit) {
return callListenerWithContext(listener, req, res, module, agent, "", readBodyAndRateLimit);
}
const result = await (0, readBodyStream_1.readBodyStream)(req, res, agent);
if (!result.success) {
return;
}
return callListenerWithContext(listener, req, res, module, agent, result.body, readBodyAndRateLimit);
};
}
// Use symbol to avoid conflicts with other properties
const countedRequest = Symbol("__zen_request_counted__");
function callListenerWithContext(listener, req, res, module, agent, body, shouldRateLimit) {
const context = (0, contextFromRequest_1.contextFromRequest)(req, body, module);
return (0, Context_1.runWithContext)(context, () => {
const context = (0, Context_1.getContext)();
if (context) {
res.on("finish", () => {
// Don't use `getContext()` in this callback
// The context won't be available when using http2
// We want the latest context (`runWithContext` updates context if there is already one)
// Since context is an object, the reference will point to the latest one
onFinishRequestHandler(req, res, agent, context);
});
}
if ((0, blockIPsAndBots_1.blockIPsAndBots)(res, agent)) {
if (context) {
// To prevent attack wave detection from checking this request
(0, Context_1.updateContext)(context, "blockedDueToIPOrBot", true);
}
// The return is necessary to prevent the listener from being called
return;
}
// Rate limiting normally happens in our framework specific middleware (e.g. express)
// For certain frameworks, e.g. next.js, we need to do this here so that it works out of the box
if (shouldRateLimit && applyRateLimiting(res)) {
return;
}
return listener(req, res);
});
}
function applyRateLimiting(res) {
const result = (0, shouldBlockRequest_1.shouldBlockRequest)();
// We don't support blocked users here because the user's code hasn't been called yet
if (result.block && result.type === "ratelimited") {
let message = "You are rate limited by Zen.";
if (result.trigger === "ip" && result.ip) {
message += ` (Your IP: ${(0, escapeHTML_1.escapeHTML)(result.ip)})`;
}
res.writeHead(429, {
"Content-Type": "text/plain",
"Retry-After": result.retryAfterSeconds.toString(),
});
res.end(message);
return true;
}
return false;
}
function onFinishRequestHandler(req, res, agent, context) {
if (req[countedRequest]) {
// The request has already been counted
// This might happen if the server has multiple listeners
return;
}
// Mark the request as counted
req[countedRequest] = true;
if (context.remoteAddress &&
agent.getConfig().isBypassedIP(context.remoteAddress)) {
return;
}
if (context.route && context.method) {
const shouldDiscover = (0, shouldDiscoverRoute_1.shouldDiscoverRoute)({
statusCode: res.statusCode,
route: context.route,
method: context.method,
});
if (shouldDiscover) {
agent.onRouteExecute(context);
}
if (shouldDiscover || context.rateLimitedEndpoint) {
agent.getInspectionStatistics().onRequest();
}
if (context.rateLimitedEndpoint) {
agent.getInspectionStatistics().onRateLimitedRequest();
agent.onRouteRateLimited(context.rateLimitedEndpoint);
}
if (context.remoteAddress &&
!context.blockedDueToIPOrBot &&
agent.getAttackWaveDetector().check(context, res.statusCode)) {
agent.onDetectedAttackWave({
request: context,
});
agent.getInspectionStatistics().onAttackWaveDetected();
}
}
}