UNPKG

@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
"use strict"; 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(); } } }