diff --git a/Makefile b/Makefile index 1f9a2d374..285145f7c 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,10 @@ koa-sqlite3: fastify-clickhouse: cd sample-apps/fastify-clickhouse && AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node app.js +.PHONY: ws-postgres +ws-postgres: + cd sample-apps/ws-postgres && AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node app.js + .PHONY: hono-prisma hono-prisma: cd sample-apps/hono-prisma && AIKIDO_DEBUG=true AIKIDO_BLOCK=true node app.js diff --git a/README.md b/README.md index 4739d7ff6..7c170da0a 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ Zen for Node.js 16+ is compatible with: * ✅ Google Cloud Functions * ✅ AWS Lambda +### Real time communication + +* ✅ [`ws`](https://www.npmjs.com/package/ws) 8.x, 7.x + ### ORMs and query builders See list above for supported database drivers. diff --git a/end2end/package-lock.json b/end2end/package-lock.json index 540f68c8e..f1307b8de 100644 --- a/end2end/package-lock.json +++ b/end2end/package-lock.json @@ -7,7 +7,8 @@ "name": "end2end", "dependencies": { "@supercharge/promise-pool": "^3.1.1", - "tap": "^18.7.0" + "tap": "^18.7.0", + "ws": "^8.18.0" } }, "node_modules/@alcalzone/ansi-tokenize": { diff --git a/end2end/package.json b/end2end/package.json index 53387d8bc..d8a995494 100644 --- a/end2end/package.json +++ b/end2end/package.json @@ -3,7 +3,8 @@ "private": true, "dependencies": { "@supercharge/promise-pool": "^3.1.1", - "tap": "^18.7.0" + "tap": "^18.7.0", + "ws": "^8.18.0" }, "scripts": { "test": "AIKIDO_CI=true tap tests/*.js --allow-empty-coverage -j 1" diff --git a/end2end/tests/ws-postgres.test.js b/end2end/tests/ws-postgres.test.js new file mode 100644 index 000000000..7c258e8a8 --- /dev/null +++ b/end2end/tests/ws-postgres.test.js @@ -0,0 +1,181 @@ +const t = require("tap"); +const { spawn } = require("child_process"); +const { resolve } = require("path"); +const timeout = require("../timeout"); +const { WebSocket } = require("ws"); + +const pathToApp = resolve(__dirname, "../../sample-apps/ws-postgres", "app.js"); + +t.test("it blocks in blocking mode", (t) => { + const server = spawn(`node`, [pathToApp, "4000"], { + env: { ...process.env, AIKIDO_DEBUG: "true", AIKIDO_BLOCKING: "true" }, + }); + + server.on("close", () => { + t.end(); + }); + + server.on("error", (err) => { + t.fail(err.message); + }); + + let stdout = ""; + server.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + let stderr = ""; + server.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + // Wait for the server to start + timeout(2000) + .then(async () => { + // Does not block normal messages + return await new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://localhost:4000`); + + ws.on("error", (err) => { + reject(err); + }); + + ws.on("open", () => { + ws.send("Hello world!"); + }); + + ws.on("message", (data) => { + const str = data.toString(); + if (str.includes("Welcome")) { + return; + } + t.match(str, /Hello world!/); + ws.close(); + resolve(); + }); + }); + }) + .then(async () => { + // Does block sql injection + return await new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://localhost:4000`); + + ws.on("error", (err) => { + reject(err); + }); + + ws.on("open", () => { + ws.send("Bye'); DELETE FROM messages;--"); + }); + + ws.on("message", (data) => { + const str = data.toString(); + if ( + str.includes("Welcome") || + str.includes("Hello") || + str.includes("Bye") + ) { + return; + } + t.match(str, /An error occurred/); + ws.close(); + resolve(); + }); + }); + }) + .then(() => { + t.match(stdout, /Starting agent/); + t.match(stderr, /Zen has blocked an SQL injection/); + }) + .catch((error) => { + t.fail(error.message); + }) + .finally(() => { + server.kill(); + }); +}); + +t.test("it does not block in non-blocking mode", (t) => { + const server = spawn(`node`, [pathToApp, "4001"], { + env: { ...process.env, AIKIDO_DEBUG: "true" }, + }); + + server.on("close", () => { + t.end(); + }); + + server.on("error", (err) => { + t.fail(err.message); + }); + + let stdout = ""; + server.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + let stderr = ""; + server.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + // Wait for the server to start + timeout(2000) + .then(async () => { + // Does not block normal messages + return await new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://localhost:4001`); + + ws.on("error", (err) => { + reject(err); + }); + + ws.on("open", () => { + ws.send("Hello world!"); + }); + + ws.on("message", (data) => { + const str = data.toString(); + if (str.includes("Welcome")) { + return; + } + t.match(str, /Hello world!/); + ws.close(); + resolve(); + }); + }); + }) + .then(async () => { + // Does block sql injection + return await new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://localhost:4001`); + + ws.on("error", (err) => { + reject(err); + }); + + ws.on("open", () => { + ws.send("Bye'); DELETE FROM messages;--"); + }); + + ws.on("message", (data) => { + const str = data.toString(); + if (str.includes("Welcome") || str.includes("Hello")) { + return; + } + t.match(str, /Bye/); + ws.close(); + resolve(); + }); + }); + }) + .then(() => { + t.match(stdout, /Starting agent/); + t.notMatch(stderr, /Zen has blocked an SQL injection/); + }) + .catch((error) => { + t.fail(error.message); + }) + .finally(() => { + server.kill(); + }); +}); diff --git a/library/agent/Context.ts b/library/agent/Context.ts index fb2e71013..eba659fc8 100644 --- a/library/agent/Context.ts +++ b/library/agent/Context.ts @@ -29,6 +29,7 @@ export type Context = { */ outgoingRequestRedirects?: { source: URL; destination: URL }[]; executedMiddleware?: boolean; + ws?: unknown; // Additional data related to WebSocket connections, like the last message received }; /** @@ -83,6 +84,7 @@ export function runWithContext(context: Context, fn: () => T) { current.graphql = context.graphql; current.xml = context.xml; current.subdomains = context.subdomains; + current.ws = context.ws; current.outgoingRequestRedirects = context.outgoingRequestRedirects; // Clear all the cached user input strings diff --git a/library/agent/Source.ts b/library/agent/Source.ts index fd07e4efa..310541997 100644 --- a/library/agent/Source.ts +++ b/library/agent/Source.ts @@ -7,6 +7,7 @@ export const SOURCES = [ "graphql", "xml", "subdomains", + "ws", ] as const; export type Source = (typeof SOURCES)[number]; diff --git a/library/agent/protect.ts b/library/agent/protect.ts index 92a853023..4eced4e6b 100644 --- a/library/agent/protect.ts +++ b/library/agent/protect.ts @@ -39,6 +39,7 @@ import { SQLite3 } from "../sinks/SQLite3"; import { XmlMinusJs } from "../sources/XmlMinusJs"; import { Hapi } from "../sources/Hapi"; import { Shelljs } from "../sinks/Shelljs"; +import { Ws } from "../sources/Ws"; import { NodeSQLite } from "../sinks/NodeSqlite"; import { BetterSQLite3 } from "../sinks/BetterSQLite3"; import { isDebugging } from "../helpers/isDebugging"; @@ -138,6 +139,7 @@ export function getWrappers() { new Fastify(), new Koa(), new ClickHouse(), + new Ws(), new Prisma(), new Function(), ]; diff --git a/library/package-lock.json b/library/package-lock.json index 53a63adb7..079570697 100644 --- a/library/package-lock.json +++ b/library/package-lock.json @@ -35,6 +35,7 @@ "@types/shell-quote": "^1.7.5", "@types/sinonjs__fake-timers": "^8.1.5", "@types/supertest": "^6.0.2", + "@types/ws": "^8.5.10", "@types/xml2js": "^0.4.14", "@typescript-eslint/eslint-plugin": "^8.4.0", "@typescript-eslint/parser": "^8.4.0", @@ -84,6 +85,7 @@ "undici-v5": "npm:undici@^5.0.0", "undici-v6": "npm:undici@^6.0.0", "undici-v7": "npm:undici@^7.0.0", + "ws": "^8.18.0", "xml-js": "^1.6.11", "xml2js": "^0.6.2" }, @@ -5347,6 +5349,16 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/xml2js": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", diff --git a/library/package.json b/library/package.json index 1d4caf72b..36c1fae44 100644 --- a/library/package.json +++ b/library/package.json @@ -68,6 +68,7 @@ "@types/shell-quote": "^1.7.5", "@types/sinonjs__fake-timers": "^8.1.5", "@types/supertest": "^6.0.2", + "@types/ws": "^8.5.10", "@types/xml2js": "^0.4.14", "@typescript-eslint/eslint-plugin": "^8.4.0", "@typescript-eslint/parser": "^8.4.0", @@ -113,6 +114,7 @@ "tap": "^18.6.1", "type-fest": "^4.24.0", "typescript": "^5.3.3", + "ws": "^8.18.0", "undici-v4": "npm:undici@^4.0.0", "undici-v5": "npm:undici@^5.0.0", "undici-v6": "npm:undici@^6.0.0", diff --git a/library/sources/HTTPServer.ts b/library/sources/HTTPServer.ts index 686bcf3b6..62229f5d9 100644 --- a/library/sources/HTTPServer.ts +++ b/library/sources/HTTPServer.ts @@ -3,7 +3,9 @@ import { Hooks } from "../agent/hooks/Hooks"; import { wrapExport } from "../agent/hooks/wrapExport"; import { wrapNewInstance } from "../agent/hooks/wrapNewInstance"; import { Wrapper } from "../agent/Wrapper"; +import { isPackageInstalled } from "../helpers/isPackageInstalled"; import { createRequestListener } from "./http-server/createRequestListener"; +import { createUpgradeListener } from "./http-server/createUpgradeListener"; import { createStreamListener } from "./http-server/http2/createStreamListener"; export class HTTPServer implements Wrapper { @@ -35,7 +37,9 @@ export class HTTPServer implements Wrapper { if (args[0] === "request") { return this.wrapRequestListener(args, module, agent); } - + if (args[0] === "upgrade") { + return [args[0], createUpgradeListener(args[1], module, agent)]; + } if (module === "http2" && args[0] === "stream") { return [args[0], createStreamListener(args[1], module, agent)]; } diff --git a/library/sources/Ws.test.ts b/library/sources/Ws.test.ts new file mode 100644 index 000000000..7c1030a2e --- /dev/null +++ b/library/sources/Ws.test.ts @@ -0,0 +1,763 @@ +import * as t from "tap"; +import { Agent } from "../agent/Agent"; +import { setInstance } from "../agent/AgentSingleton"; +import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting"; +import { Token } from "../agent/api/Token"; +import { setUser } from "../agent/context/user"; +import { LoggerNoop } from "../agent/logger/LoggerNoop"; +import { Ws } from "./Ws"; +import { FileSystem } from "../sinks/FileSystem"; +import { HTTPServer } from "./HTTPServer"; + +const agent = new Agent( + true, + new LoggerNoop(), + new ReportingAPIForTesting({ + success: true, + endpoints: [ + { + method: "GET", + route: "/rate-limited", + forceProtectionOff: false, + rateLimiting: { + windowSizeInMS: 2000, + maxRequests: 2, + enabled: true, + }, + }, + ], + blockedUserIds: ["567"], + configUpdatedAt: 0, + heartbeatIntervalInMS: 10 * 60 * 1000, + allowedIPAddresses: ["4.3.2.1"], + }), + new Token("123"), + undefined +); +agent.start([new Ws(), new FileSystem(), new HTTPServer()]); +setInstance(agent); + +process.env.AIKIDO_MAX_BODY_SIZE_MB = "1"; + +import { WebSocketServer, WebSocket } from "ws"; +import { getContext } from "../agent/Context"; +import { createServer, Server } from "http"; +import { parseWsData } from "./ws/parseWSData"; +import { AddressInfo } from "net"; + +// Method to run a sample WebSocket server with different configurations +function runServer( + useHttpServer: boolean, + eventListenerType: "on" | "addEventListener" | "onlong" | "once" = "on", + customUpgrade = false +) { + let wss: WebSocketServer; + let httpServer: Server | undefined; + if (!useHttpServer) { + wss = new WebSocket.Server({ port: 0 }); + } else { + httpServer = createServer(); + if (!customUpgrade) { + wss = new WebSocketServer({ server: httpServer }); + } else { + if (!useHttpServer) { + throw new Error("Custom upgrade requires http server"); + } + wss = new WebSocketServer({ noServer: true }); + } + } + + const onEvent = (ws: WebSocket) => { + const onMessage = () => { + ws.send(JSON.stringify(getContext())); + }; + + if (eventListenerType === "addEventListener") { + ws.addEventListener("message", onMessage, { once: true }); + } else if (eventListenerType === "onlong") { + ws.onmessage = onMessage; + } else { + ws.on("message", onMessage); + } + + ws.on("ping", (data) => { + // Send back the context + ws.send(JSON.stringify(getContext())); + }); + + ws.on("pong", (data) => { + // Send back the context + ws.send(JSON.stringify(getContext())); + }); + + ws.on("close", (code, reason) => { + if (code === 1001 && Buffer.isBuffer(reason) && reason.length) { + t.same(reason.toString(), getContext()?.ws); + } + }); + }; + + if (eventListenerType === "addEventListener") { + wss.addListener("connection", onEvent); + } else if (eventListenerType === "once") { + wss.once("connection", onEvent); + } else { + wss.on("connection", onEvent); + } + + if (httpServer) { + if (customUpgrade) { + httpServer.on("upgrade", function upgrade(request, socket, head) { + const { pathname } = new URL( + request.url || "", + `http://${request.headers.host}` + ); + + if (pathname === "/block-user") { + setUser({ id: "567" }); + } + + wss.handleUpgrade(request, socket, head, function done(ws) { + wss.emit("connection", ws, request); + }); + }); + } + + httpServer.listen(0); + } + + return { + port: !customUpgrade + ? (wss.address() as AddressInfo).port + : (httpServer!.address() as AddressInfo).port, + close: () => { + wss.close(); + if (httpServer) { + httpServer.close(); + } + }, + }; +} + +const testServer1 = runServer(false); + +const getExpectedContext = (port: number) => { + return { + url: "/", + method: "GET", + headers: { + "sec-websocket-version": "13", + connection: "Upgrade", + upgrade: "websocket", + "sec-websocket-extensions": "permessage-deflate; client_max_window_bits", + host: `localhost:${port}`, + }, + route: "/", + query: {}, + source: "ws.connection", + routeParams: {}, + cookies: {}, + body: undefined, + }; +}; + +t.test("Connect to WebSocket server and get context", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("getContext"); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer1.port), + ws: "getContext", + }); + t.match(context.remoteAddress, /(::ffff:127\.0\.0\.1|127\.0\.0\.1|::1)/); + + ws.close(); + t.end(); + }); +}); + +t.test("Connect to WebSocket server and send json object", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}/path?test=abc`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send(JSON.stringify({ test: "test1" })); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer1.port), + url: "/path?test=abc", + query: { test: "abc" }, + ws: { test: "test1" }, + }); + + ws.close(); + t.end(); + }); +}); + +t.test("Connect to WebSocket server and send buffer", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}`, { + headers: { + cookie: "test=cookievalue", + }, + }); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send(Buffer.from("test-buffer")); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer1.port), + ws: "test-buffer", + cookies: { test: "cookievalue" }, + }); + + ws.close(); + t.end(); + }); +}); + +t.test("Connect to WebSocket server and send Uint8Array", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send(new TextEncoder().encode("test-text-encoder")); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer1.port), + ws: "test-text-encoder", + }); + + ws.close(); + t.end(); + }); +}); + +t.test("Connect to WebSocket server and send non utf-8 Uint8Array", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send(new Uint8Array([0x80, 0x81, 0x82, 0x83])); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer1.port), + ws: undefined, + }); + + ws.close(); + t.end(); + }); +}); + +t.test( + "Connect to WebSocket server and send text as Blob", + { skip: !global.Blob ? "Blob is not available" : false }, + (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + // @ts-expect-error types say we are not allowed to send a Blob? + ws.send(new Blob(["test-blob"])); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer1.port), + ws: "test-blob", + }); + + ws.close(); + t.end(); + }); + } +); + +t.test( + "Connect to WebSocket server and send binary as Blob", + { skip: !global.Blob ? "Blob is not available" : false }, + (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + // @ts-expect-error types say we are not allowed to send a Blob? + ws.send(new Blob([new Uint8Array([0x80, 0x81, 0x82, 0x83])])); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer1.port), + ws: undefined, + }); + + ws.close(); + t.end(); + }); + } +); + +// We use the function directly to test because the websocket client converts blobs to array buffers (depending on the version?) +t.test( + "Pass text blob to onMessageEvent", + { skip: !global.Blob ? "Blob is not available" : false }, + async (t) => { + const result = await parseWsData([new Blob(["test-blob"])]); + t.same(result.data, "test-blob"); + t.same(result.tooLarge, false); + } +); + +t.test( + "Pass binary blob to onMessageEvent", + { skip: !global.Blob ? "Blob is not available" : false }, + async (t) => { + const result = await parseWsData([ + new Blob([new Uint8Array([0x80, 0x81, 0x82, 0x83])]), + ]); + t.same(result.data, undefined); + t.same(result.tooLarge, false); + } +); + +t.test( + "Pass too large blob to onMessageEvent", + { skip: !global.Blob ? "Blob is not available" : false }, + async (t) => { + const result = await parseWsData([new Blob(["a".repeat(2 * 1024 * 1024)])]); + t.same(result.data, undefined); + t.same(result.tooLarge, true); + } +); + +t.test("Pass buffer array to onMessageEvent", async (t) => { + const result = await parseWsData([ + [Buffer.from("test-buffer-1"), Buffer.from("test-buffer-2")], + ]); + t.same(result.data, "test-buffer-1test-buffer-2"); + t.same(result.tooLarge, false); +}); + +t.test("Pass buffer array with non utf-8 to onMessageEvent", async (t) => { + const result = await parseWsData([ + [Buffer.from("test-buffer-1"), Buffer.from([0x80, 0x81, 0x82, 0x83])], + ]); + t.same(result.data, undefined); + t.same(result.tooLarge, false); +}); + +t.test("Pass too large buffer array to onMessageEvent", async (t) => { + const result = await parseWsData([ + [Buffer.from("a".repeat(2 * 1024 * 1024))], + ]); + t.same(result.data, undefined); + t.same(result.tooLarge, true); +}); + +t.test("Pass no data to onMessageEvent", async (t) => { + const result = await parseWsData([]); + t.same(result.data, undefined); + t.same(result.tooLarge, false); +}); + +t.test("Pass unsupported data to onMessageEvent", async (t) => { + const result = await parseWsData([new Date()]); + t.same(result.data, undefined); + t.same(result.tooLarge, false); +}); + +t.test("Pass unexpected data to onMessageEvent is ignored", async (t) => { + const result = await parseWsData([ + [new Date(), 123, { test: "test" }, null, undefined], + ]); + t.same(result.data, undefined); + t.same(result.tooLarge, false); +}); + +t.test("Send ping with data to WebSocket server", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.ping("test-ping"); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer1.port), + ws: "test-ping", + }); + + ws.close(); + t.end(); + }); +}); + +t.test("Send close with data to WebSocket server", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.close(1001, "test-close"); + t.end(); + }); +}); + +t.test("Send close with data in buffer to WebSocket server", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.close(1001, Buffer.from("test-close")); + t.end(); + }); +}); + +t.test("Send pong with data to WebSocket server", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer1.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.pong(JSON.stringify({ test: "pong" })); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer1.port), + ws: { test: "pong" }, + }); + + ws.close(); + testServer1.close(); + t.end(); + }); +}); + +const testServer2 = runServer(true); + +t.test("Send message to WebSocket server with http server", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer2.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("getContextHTTP"); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer2.port), + ws: "getContextHTTP", + }); + + t.match(context.remoteAddress, /(::ffff:127\.0\.0\.1|127\.0\.0\.1|::1)/); + + ws.close(); + testServer2.close(); + t.end(); + }); +}); + +const testServer3 = runServer(false, "addEventListener"); + +t.test("Send message to WebSocket server using addEventListener", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer3.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("getContextEvent"); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer3.port), + ws: "getContextEvent", + }); + t.match(context.remoteAddress, /(::ffff:127\.0\.0\.1|127\.0\.0\.1|::1)/); + + ws.close(); + testServer3.close(); + t.end(); + }); +}); + +const testServer4 = runServer(false, "onlong"); + +t.test("Send message to WebSocket server using onmessage", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer4.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("getContextOnMessage"); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer4.port), + ws: "getContextOnMessage", + }); + t.match(context.remoteAddress, /(::ffff:127\.0\.0\.1|127\.0\.0\.1|::1)/); + + ws.close(); + t.end(); + }); +}); + +t.test("Send more than 2MB of data to WebSocket server", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer4.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("a".repeat(2.5 * 1024 * 1024)); + }); + + ws.on("message", (data) => { + t.match( + data.toString(), + "WebSocket message size exceeded the maximum allowed size. Use AIKIDO_MAX_BODY_SIZE_MB to increase the limit." + ); + ws.close(); + testServer4.close(); + t.end(); + }); +}); + +// Custom http upgrade +const testServer5 = runServer(true, "on", true); + +t.test("Test custom http upgrade", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer5.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("getContextOnMessage"); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer5.port), + ws: "getContextOnMessage", + }); + + ws.close(); + t.end(); + }); +}); + +t.test("Test block user on custom http upgrade", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer5.port}/block-user`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("Hi!"); + }); + + ws.on("close", (code, reason) => { + t.same(code, 3000); + t.match(reason.toString(), /You are blocked by Aikido firewall/); + + ws.close(); + t.end(); + }); +}); + +t.test("Test rate limiting on WebSocket server - 1st request", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer5.port}/rate-limited`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("getContextOnMessage"); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer5.port), + ws: "getContextOnMessage", + }); + + ws.close(); + t.end(); + }); +}); + +t.test("Test rate limiting on WebSocket server - 2nd request", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer5.port}/rate-limited`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("getContextOnMessage"); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer5.port), + ws: "getContextOnMessage", + }); + + ws.close(); + + t.end(); + }); +}); + +t.test("Test rate limiting on WebSocket server - 3rd request", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer5.port}/rate-limited`); + + ws.on("unexpected-response", (req, res) => { + t.same(res.statusCode, 429); + t.same(res.statusMessage, "Too Many Requests"); + testServer5.close(); + t.end(); + }); +}); + +// Check once +const testServer6 = runServer(false, "once", false); + +t.test("Send message to WebSocket server using once", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer6.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("getContextOnce"); + }); + + ws.on("message", (data) => { + const context = JSON.parse(data.toString()); + + t.match(context, { + ...getExpectedContext(testServer6.port), + ws: "getContextOnce", + }); + t.match(context.remoteAddress, /(::ffff:127\.0\.0\.1|127\.0\.0\.1|::1)/); + + ws.close(); + t.end(); + }); +}); + +t.test("Send message to WebSocket server using once - 2nd request", (t) => { + const ws = new WebSocket(`ws://localhost:${testServer6.port}`); + + ws.on("error", (err) => { + t.fail(err); + }); + + ws.on("open", () => { + ws.send("getContextOnce"); + + setTimeout(() => { + ws.close(); + testServer6.close(); + t.end(); + }, 150); + }); + + ws.on("message", (data) => { + t.fail("Should not receive message"); + }); +}); diff --git a/library/sources/Ws.ts b/library/sources/Ws.ts new file mode 100644 index 000000000..ad44e0cf0 --- /dev/null +++ b/library/sources/Ws.ts @@ -0,0 +1,33 @@ +import { Agent } from "../agent/Agent"; +import { Hooks } from "../agent/hooks/Hooks"; +import { wrapExport } from "../agent/hooks/wrapExport"; +import { wrapNewInstance } from "../agent/hooks/wrapNewInstance"; +import { Wrapper } from "../agent/Wrapper"; +import { wrapHandleUpgradeCallback } from "./ws/wrapHandleUpgrade"; + +export class Ws implements Wrapper { + private wrapUpgrade(args: unknown[], agent: Agent) { + if (args.length < 4 || typeof args[3] !== "function") { + return args; + } + + args[3] = wrapHandleUpgradeCallback(args[3], agent); + + return args; + } + + wrap(hooks: Hooks) { + hooks + .addPackage("ws") + .withVersion("^8.0.0 || ^7.0.0") + .onRequire((exports, pkgInfo) => { + for (const server of ["WebSocketServer", "Server"]) { + wrapNewInstance(exports, server, pkgInfo, (instance) => { + wrapExport(instance, "handleUpgrade", pkgInfo, { + modifyArgs: (args, agent) => this.wrapUpgrade(args, agent), + }); + }); + } + }); + } +} diff --git a/library/sources/http-server/contextFromRequest.ts b/library/sources/http-server/contextFromRequest.ts index eee06c16d..0712ddf72 100644 --- a/library/sources/http-server/contextFromRequest.ts +++ b/library/sources/http-server/contextFromRequest.ts @@ -8,7 +8,7 @@ import { tryParseURLParams } from "../../helpers/tryParseURLParams"; export function contextFromRequest( req: IncomingMessage, body: string | undefined, - module: string + source: string ): Context { const queryObject: Record = {}; if (req.url) { @@ -33,7 +33,7 @@ export function contextFromRequest( headers: req.headers, route: req.url ? buildRouteFromURL(req.url) : undefined, query: queryObject, - source: `${module}.createServer`, + source, routeParams: {}, cookies: req.headers?.cookie ? parse(req.headers.cookie) : {}, body: parsedBody, diff --git a/library/sources/http-server/createRequestListener.ts b/library/sources/http-server/createRequestListener.ts index 4f74448a8..17ca0f0df 100644 --- a/library/sources/http-server/createRequestListener.ts +++ b/library/sources/http-server/createRequestListener.ts @@ -51,7 +51,7 @@ function callListenerWithContext( agent: Agent, body: string ) { - const context = contextFromRequest(req, body, module); + const context = contextFromRequest(req, body, `${module}.createServer`); return runWithContext(context, () => { // This method is called when the response is finished and discovers the routes for display in the dashboard diff --git a/library/sources/http-server/createUpgradeListener.ts b/library/sources/http-server/createUpgradeListener.ts new file mode 100644 index 000000000..23de8021b --- /dev/null +++ b/library/sources/http-server/createUpgradeListener.ts @@ -0,0 +1,42 @@ +import { Duplex } from "stream"; +import { Agent } from "../../agent/Agent"; +import type { IncomingMessage } from "http"; +import { contextFromRequest } from "./contextFromRequest"; +import { getContext, runWithContext } from "../../agent/Context"; +import { shouldRateLimitRequest } from "../../ratelimiting/shouldRateLimitRequest"; +import { escapeHTML } from "../../helpers/escapeHTML"; + +export function createUpgradeListener( + listener: Function, + module: string, + agent: Agent +) { + return async function upgradeListener( + req: IncomingMessage, + socket: Duplex, + head: Buffer + ) { + const context = contextFromRequest(req, undefined, `${module}.onUpgrade`); + + return runWithContext(context, () => { + const context = getContext(); + + if (!context) { + return listener(req, socket, head); + } + + const result = shouldRateLimitRequest(context, agent); + + if (result.block) { + let message = "You are rate limited by Aikido firewall."; + if (result.trigger === "ip") { + message += ` (Your IP: ${escapeHTML(context.remoteAddress!)})`; + } + + return socket.end(`HTTP/1.1 429 Too Many Requests\r\n\r\n${message}`); + } + + return listener(req, socket, head); + }); + }; +} diff --git a/library/sources/ws/contextFromConnection.ts b/library/sources/ws/contextFromConnection.ts new file mode 100644 index 000000000..5484cd038 --- /dev/null +++ b/library/sources/ws/contextFromConnection.ts @@ -0,0 +1,32 @@ +import { Context } from "../../agent/Context"; +import { buildRouteFromURL } from "../../helpers/buildRouteFromURL"; +import { getIPAddressFromRequest } from "../../helpers/getIPAddressFromRequest"; +import { parse } from "../../helpers/parseCookies"; +import type { IncomingMessage } from "http"; +import { tryParseURLParams } from "../../helpers/tryParseURLParams"; + +export function contextFromConnection(req: IncomingMessage): Context { + const queryObject: Record = {}; + if (req.url) { + const params = tryParseURLParams(req.url); + for (const [key, value] of params.entries()) { + queryObject[key] = value; + } + } + + return { + url: req.url, + method: req.method, + headers: req.headers, + route: req.url ? buildRouteFromURL(req.url) : undefined, + query: queryObject, + source: `ws.connection`, + routeParams: {}, + cookies: req.headers?.cookie ? parse(req.headers.cookie) : {}, + body: undefined, + remoteAddress: getIPAddressFromRequest({ + headers: req.headers, + remoteAddress: req.socket?.remoteAddress, + }), + }; +} diff --git a/library/sources/ws/parseWSData.ts b/library/sources/ws/parseWSData.ts new file mode 100644 index 000000000..81a55892d --- /dev/null +++ b/library/sources/ws/parseWSData.ts @@ -0,0 +1,117 @@ +/* eslint-disable max-lines-per-function */ +import { Agent } from "../../agent/Agent"; +import { getMaxBodySize } from "../../helpers/getMaxBodySize"; + +type WsData = ArrayBuffer | Blob | Buffer | Buffer[] | string; + +/** + * If the ws event arg is an event object, extract the data from it + */ +function extractWsDataFromEvent(arg: unknown): WsData { + if ( + typeof arg === "object" && + arg !== null && + "data" in arg && + "type" in arg && + "target" in arg + ) { + return arg.data as WsData; + } + return arg as WsData; +} + +/** + * Tried to parse the data as JSON, if it fails it returns the original data + */ +function tryJSONParse(data: string) { + try { + return JSON.parse(data); + } catch (e) { + return data; + } +} + +function isBufferArray(data: WsData): boolean { + return Array.isArray(data) && data.every((d) => Buffer.isBuffer(d)); +} + +function isMessageDataTooLarge(data: WsData) { + const maxMsgSize = getMaxBodySize(); + let size = -1; + + if (global.Blob && data instanceof Blob) { + size = data.size; + } else if (Buffer.isBuffer(data) || data instanceof ArrayBuffer) { + size = data.byteLength; + } else if (typeof data === "string") { + size = Buffer.byteLength(data, "utf8"); + } else if (isBufferArray(data)) { + // @ts-expect-error Typescript does not detect that data can not be an blob because of the global.Blob check required for Node.js 16 + size = Buffer.concat(data).byteLength; + } + + return size > maxMsgSize; +} + +export async function parseWsData( + args: any[], + agent?: Agent +): Promise<{ data: string | object | undefined; tooLarge: boolean }> { + if (!args || !args.length) { + return { data: undefined, tooLarge: false }; + } + const data = extractWsDataFromEvent(args[0]); + let messageStr: string | undefined; + + try { + const tooLarge = isMessageDataTooLarge(data); + if (tooLarge) { + return { data: undefined, tooLarge: true }; + } + + // Handle Blob + if (global.Blob && data instanceof Blob) { + messageStr = await data.text(); + if (typeof messageStr !== "string" || messageStr.includes("\uFFFD")) { + return { data: undefined, tooLarge: false }; + } + } // Decode ArrayBuffer or Buffer to string if it is valid utf-8 (or ascii) + else if (Buffer.isBuffer(data) || data instanceof ArrayBuffer) { + const decoder = new TextDecoder("utf-8", { + fatal: true, // Throw error if buffer is not valid utf-8 + }); + + messageStr = decoder.decode(data); + } //Check if is string + else if (typeof data === "string") { + messageStr = data; + } // Check if is array of Buffers, concat and decode + else if (isBufferArray(data)) { + // @ts-expect-error Typescript does not detect that data can not be an blob because of the global.Blob check required for Node.js 16 + const concatenatedBuffer = Buffer.concat(data); + const decoder = new TextDecoder("utf-8", { + fatal: true, + }); + + messageStr = decoder.decode(concatenatedBuffer); + } else { + // Data type not supported + throw new Error("Unsupported ws message data type"); + } + } catch (e) { + if (agent) { + if (e instanceof Error) { + agent.log(`Failed to parse WebSocket message: ${e.message}`); + } else { + agent.log(`Failed to parse WebSocket message`); + } + } + return { data: undefined, tooLarge: false }; + } + + if (typeof messageStr !== "string") { + return { data: undefined, tooLarge: false }; + } + + return { data: tryJSONParse(messageStr), tooLarge: false }; +} diff --git a/library/sources/ws/wrapHandleUpgrade.ts b/library/sources/ws/wrapHandleUpgrade.ts new file mode 100644 index 000000000..d1970a5c9 --- /dev/null +++ b/library/sources/ws/wrapHandleUpgrade.ts @@ -0,0 +1,49 @@ +import type { WebSocket } from "ws"; +import { Agent } from "../../agent/Agent"; +import { getContext, runWithContext } from "../../agent/Context"; +import { IncomingMessage } from "http"; +import { contextFromConnection } from "./contextFromConnection"; +import { wrapSocketEvent } from "./wrapSocketEvents"; + +// Wraps the WebSocketServer handleUpgrade callback, thats called when a new connection is established +export function wrapHandleUpgradeCallback(handler: any, agent: Agent): any { + return async (socket: WebSocket, request: IncomingMessage) => { + const context = contextFromConnection(request); + + return runWithContext(context, () => { + // Even though we already have the context, we need to get it again + // The context from `contextFromRequest` will never return a user + // The user will be carried over from the previous context + const context = getContext(); + + if (!context) { + return handler(socket, request); + } + + if (context.user && agent.getConfig().isUserBlocked(context.user.id)) { + return socket.close(3000, "You are blocked by Aikido firewall."); + } + + const methodNames = [ + "on", + "once", + "addEventListener", + "onmessage", + "onclose", + "onerror", + "onopen", + ]; + + for (const methodName of methodNames) { + const key = methodName as keyof WebSocket; + if (typeof socket[key] !== "function") { + continue; + } + // @ts-expect-error keyof does not exclude readonly properties + socket[key] = wrapSocketEvent(socket[key], socket, agent); + } + + return handler(socket, request); + }); + }; +} diff --git a/library/sources/ws/wrapSocketEvents.ts b/library/sources/ws/wrapSocketEvents.ts new file mode 100644 index 000000000..bdc937f80 --- /dev/null +++ b/library/sources/ws/wrapSocketEvents.ts @@ -0,0 +1,96 @@ +/* eslint-disable max-lines-per-function */ +import { AsyncResource } from "async_hooks"; +import { getContext, updateContext } from "../../agent/Context"; +import type { WebSocket } from "ws"; +import { Agent } from "../../agent/Agent"; +import { parseWsData } from "./parseWSData"; + +export function wrapSocketEvent( + handler: any, + socket: WebSocket, + agent: Agent +): any { + return function wrapped() { + const applyHandler = (args: unknown[] | undefined = undefined) => { + return handler.apply( + // @ts-expect-error We don't now the type of this + this, + // eslint-disable-next-line prefer-rest-params + args || arguments + ); + }; + + const context = getContext(); + // We expect the context to be set by the connection handler + if (!context) { + return applyHandler(); + } + + // eslint-disable-next-line prefer-rest-params + const args = Array.from(arguments); + if ( + args.length >= 2 && + typeof args[0] === "string" && + typeof args[1] === "function" + ) { + args[1] = AsyncResource.bind( + wrapSocketEventHandler(args[0], args[1], socket, agent) + ); + + return applyHandler(args); + } + + return applyHandler(); + }; +} + +function wrapSocketEventHandler( + event: string, + handler: any, + socket: WebSocket, + agent: Agent +): any { + return async function wrappedHandler() { + const applyHandler = () => { + return handler.apply( + // @ts-expect-error We don't now the type of this + this, + // eslint-disable-next-line prefer-rest-params + arguments + ); + }; + + const context = getContext(); + if (!context) { + return applyHandler(); + } + + let parsedData; + + // Events with data + if (event === "message" || event === "ping" || event === "pong") { + // eslint-disable-next-line prefer-rest-params + parsedData = await parseWsData(Array.from(arguments), agent); + } + + // eslint-disable-next-line prefer-rest-params + if (event === "close" && arguments.length > 1) { + // eslint-disable-next-line prefer-rest-params + parsedData = await parseWsData([arguments[1]], agent); + } + + if (parsedData) { + if (parsedData.tooLarge) { + socket.send( + "WebSocket message size exceeded the maximum allowed size. Use AIKIDO_MAX_BODY_SIZE_MB to increase the limit." + ); + return; // Do not call the original handler + } + if (parsedData.data) { + updateContext(context, "ws", parsedData.data); + } + } + + return applyHandler(); + }; +} diff --git a/package-lock.json b/package-lock.json index 329b9f92f..209389ebb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,128 @@ "prettier": "^3.2.4" } }, + "benchmarks/nosql-injection": { + "name": "nosql-injection-benchmark", + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "mongodb": "^6.3.0", + "percentile": "^1.6.0" + } + }, + "benchmarks/shell-injection": { + "name": "shell-injection-benchmark", + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build" + } + }, + "benchmarks/sql-injection": { + "name": "sql-injection-benchmark", + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build" + } + }, + "build": { + "name": "@aikidosec/firewall", + "version": "0.0.0", + "extraneous": true, + "license": "AGPL-3.0-or-later", + "engines": { + "node": ">=16" + } + }, + "end2end": { + "extraneous": true, + "dependencies": { + "@supercharge/promise-pool": "^3.1.1", + "tap": "^18.7.0", + "ws": "^8.18.0" + } + }, + "library": { + "name": "@aikidosec/firewall", + "version": "0.0.0", + "extraneous": true, + "license": "AGPL-3.0-or-later", + "devDependencies": { + "@clickhouse/client": "^1.7.0", + "@fastify/cookie": "^10.0.0", + "@google-cloud/functions-framework": "^3.3.0", + "@google-cloud/pubsub": "^4.3.3", + "@graphql-tools/executor": "^1.3.2", + "@hapi/hapi": "^21.3.10", + "@hono/node-server": "^1.12.2", + "@koa/bodyparser": "^5.1.1", + "@koa/router": "^13.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@types/aws-lambda": "^8.10.131", + "@types/cookie-parser": "^1.4.6", + "@types/express": "^4.17.21", + "@types/follow-redirects": "^1.14.4", + "@types/ip": "^1.1.3", + "@types/koa": "^2.15.0", + "@types/koa__router": "^12.0.4", + "@types/mysql": "^2.15.25", + "@types/needle": "^3.3.0", + "@types/node": "^22.3.0", + "@types/pg": "^8.11.0", + "@types/qs": "^6.9.11", + "@types/shell-quote": "^1.7.5", + "@types/sinonjs__fake-timers": "^8.1.5", + "@types/supertest": "^6.0.2", + "@types/ws": "^8.5.10", + "@types/xml2js": "^0.4.14", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", + "aws-sdk": "^2.1595.0", + "axios": "^1.7.3", + "better-sqlite3": "^11.2.0", + "bson-objectid": "^2.0.4", + "cookie-parser": "^1.4.6", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.1.3", + "express": "^5.0.0", + "express-async-handler": "^1.2.0", + "fast-xml-parser": "^4.4.0", + "fastify": "^5.0.0", + "follow-redirects": "^1.15.6", + "graphql": "^16.8.2", + "hono": "^4.4.2", + "koa": "^2.15.3", + "koa-router": "^12.0.1", + "mariadb": "^3.3.2", + "mongodb": "^6.3.0", + "mysql": "^2.18.1", + "mysql2": "^3.10.0", + "needle": "^3.3.1", + "node-fetch": "^2", + "percentile": "^1.6.0", + "pg": "^8.11.3", + "postgres": "^3.4.4", + "prettier": "^3.2.4", + "shell-quote": "^1.8.1", + "shelljs": "^0.8.5", + "sqlite3": "^5.1.7", + "supertest": "^6.3.4", + "tap": "^18.6.1", + "type-fest": "^4.24.0", + "typescript": "^5.3.3", + "undici": "^6.12.0", + "ws": "^8.18.0", + "xml-js": "^1.6.11", + "xml2js": "^0.6.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/prettier": { "version": "3.2.5", "dev": true, @@ -22,6 +144,193 @@ "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } + }, + "sample-apps/cloud-functions-v1-mongodb": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@google-cloud/functions-framework": "^3.3.0", + "mongodb": "^6.3.0" + } + }, + "sample-apps/cloud-functions-v2-mongodb": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@google-cloud/functions-framework": "^3.3.0", + "mongodb": "^6.3.0" + } + }, + "sample-apps/express-graphql": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "graphql": "^16.8.2", + "graphql-http": "^1.22.1", + "morgan": "^1.10.0", + "mysql2": "^3.10.0" + } + }, + "sample-apps/express-mariadb": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "cookie-parser": "^1.4.6", + "dotenv": "^16.4.1", + "express": "^4.19.2", + "express-async-handler": "^1.2.0", + "mariadb": "^3.2.3", + "morgan": "^1.10.0" + } + }, + "sample-apps/express-mongodb": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@opentelemetry/api": "^1.8.0", + "@opentelemetry/auto-instrumentations-node": "^0.44.0", + "cookie-parser": "^1.4.6", + "dotenv": "^16.4.1", + "express": "^4.19.2", + "express-async-handler": "^1.2.0", + "mongodb": "~6.3.0", + "morgan": "^1.10.0" + } + }, + "sample-apps/express-mongoose": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "cookie-parser": "^1.4.6", + "dotenv": "^16.4.1", + "express": "^4.19.2", + "express-async-handler": "^1.2.0", + "mongoose": "^8.1.1", + "morgan": "^1.10.0" + } + }, + "sample-apps/express-mysql": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@sentry/node": "^7", + "dotenv": "^16.4.1", + "express": "^5.0.0", + "express-async-handler": "^1.2.0", + "morgan": "^1.10.0", + "mysql": "^2.18.1", + "xml-js": "^1.6.11" + } + }, + "sample-apps/express-mysql2": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "dotenv": "^16.4.1", + "express": "^4.19.2", + "express-async-handler": "^1.2.0", + "morgan": "^1.10.0", + "mysql2": "^3.10.0" + } + }, + "sample-apps/express-path-traversal": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "dotenv": "^16.4.1", + "express": "^4.19.2", + "express-async-handler": "^1.2.0", + "morgan": "^1.10.0" + } + }, + "sample-apps/express-postgres": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "dd-trace": "^4.49.0", + "dotenv": "^16.4.1", + "express": "^4.19.2", + "express-async-handler": "^1.2.0", + "morgan": "^1.10.0", + "pg": "^8.11.3" + } + }, + "sample-apps/hapi-postgres": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@hapi/hapi": "^21.3.10", + "pg": "^8.11.3" + } + }, + "sample-apps/hono-mongodb": { + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@hono/node-server": "^1.11.2", + "hono": "^4.4.2", + "mongodb": "^6.3.0" + } + }, + "sample-apps/hono-sqlite3": { + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@hono/node-server": "^1.11.2", + "hono": "^4.4.2", + "sqlite3": "^5.1.7" + } + }, + "sample-apps/hono-xml": { + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@hono/node-server": "^1.11.2", + "fast-xml-parser": "^4.4.0", + "hono": "^4.4.2", + "mysql2": "^3.10.0", + "xml2js": "^0.6.2" + } + }, + "sample-apps/lambda-mongodb": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "mongodb": "^6.3.0" + } + }, + "sample-apps/pubsub-mongodb": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "@google-cloud/pubsub": "^4.3.3", + "mongodb": "^6.3.0" + } + }, + "sample-apps/ws-postgres": { + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "express": "^4.19.2", + "pg": "^8.11.3", + "ws": "^8.18.0" + } } } } diff --git a/sample-apps/ws-postgres/README.md b/sample-apps/ws-postgres/README.md new file mode 100644 index 000000000..d000d4fea --- /dev/null +++ b/sample-apps/ws-postgres/README.md @@ -0,0 +1,9 @@ +# ws-postgres + +A minimal chat app using `ws` with vulnerabilities. + +WARNING: This application contains security issues and should not be used in production (or taken as an example of how to write secure code). + +In the root directory run `make ws-postgres` to start the server. + +Hint: All old messages are deleted on app start. diff --git a/sample-apps/ws-postgres/app.js b/sample-apps/ws-postgres/app.js new file mode 100644 index 000000000..56139b48d --- /dev/null +++ b/sample-apps/ws-postgres/app.js @@ -0,0 +1,113 @@ +require("@aikidosec/firewall"); + +const express = require("express"); +const { Client } = require("pg"); +const http = require("http"); +const WebSocket = require("ws"); + +require("@aikidosec/firewall/nopp"); + +async function createConnection() { + const client = new Client({ + user: "root", + host: "127.0.0.1", + database: "main_db", + password: "password", + port: 27016, + }); + + await client.connect(); + await client.query(` + CREATE TABLE IF NOT EXISTS messages ( + id SERIAL PRIMARY KEY, + content TEXT NOT NULL, + timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + ); + `); + + // Delete all messages + await client.query("DELETE FROM messages"); + + return client; +} + +async function main(port) { + const db = await createConnection(); + + const app = express(); + const server = http.createServer(app); + + app.use(express.static(__dirname + "/public")); + + app.get("/", (req, res) => { + res.sendFile(__dirname + "/index.html"); + }); + + const wss = new WebSocket.Server({ server }); + + wss.on("connection", (ws) => { + // Send chat history to the new client + db.query( + "SELECT content, timestamp FROM messages ORDER BY timestamp", + (err, res) => { + if (!err) { + res.rows.forEach((row) => { + const time = new Date(row.timestamp).toLocaleTimeString(); + ws.send(`[${time}] ${row.content}`); + }); + } + } + ); + + // Handle incoming messages + ws.on("message", (message) => { + try { + const time = new Date().toLocaleTimeString(); + // Broadcast message to all clients + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(`[${time}] ${message}`); + } + }); + // Insert message into the database + db.query( + `INSERT INTO messages (content) VALUES ('${message}') RETURNING *`, + (err, res) => { + if (err) { + ws.send("An error occurred"); + } + } + ); + } catch (err) { + console.error(err); + ws.send("An error occurred"); + } + }); + + ws.send("Welcome to the chat!"); + }); + + return new Promise((resolve, reject) => { + try { + server.listen(port, () => { + console.log(`Listening on port ${port}`); + resolve(); + }); + } catch (err) { + reject(err); + } + }); +} + +function getPort() { + const port = parseInt(process.argv[2], 10) || 4000; + + if (isNaN(port)) { + console.error("Invalid port"); + process.exit(1); + } + + return port; +} + +main(getPort()); diff --git a/sample-apps/ws-postgres/package-lock.json b/sample-apps/ws-postgres/package-lock.json new file mode 100644 index 000000000..a11a3769c --- /dev/null +++ b/sample-apps/ws-postgres/package-lock.json @@ -0,0 +1,954 @@ +{ + "name": "ws-postgres", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ws-postgres", + "version": "1.0.0", + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "express": "^4.19.2", + "pg": "^8.11.3", + "ws": "^8.18.0" + } + }, + "../../build": { + "name": "@aikidosec/firewall", + "version": "0.0.0", + "license": "AGPL-3.0-or-later", + "engines": { + "node": ">=16" + } + }, + "node_modules/@aikidosec/firewall": { + "resolved": "../../build", + "link": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/sample-apps/ws-postgres/package.json b/sample-apps/ws-postgres/package.json new file mode 100644 index 000000000..b2de70960 --- /dev/null +++ b/sample-apps/ws-postgres/package.json @@ -0,0 +1,13 @@ +{ + "name": "ws-postgres", + "version": "1.0.0", + "description": "A vulnerable app to test out SQL Injection with postgres", + "main": "app.js", + "private": true, + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "express": "^4.19.2", + "pg": "^8.11.3", + "ws": "^8.18.0" + } +} diff --git a/sample-apps/ws-postgres/public/client.js b/sample-apps/ws-postgres/public/client.js new file mode 100644 index 000000000..d0db8e43f --- /dev/null +++ b/sample-apps/ws-postgres/public/client.js @@ -0,0 +1,24 @@ +const chat = document.getElementById("chat"); +const messageInput = document.getElementById("message"); + +const ws = new WebSocket("ws://localhost:4000"); + +ws.onmessage = (event) => { + const message = document.createElement("div"); + message.textContent = event.data; + chat.appendChild(message); + chat.scrollTop = chat.scrollHeight; +}; + +ws.onclose = () => { + messageInput.disabled = true; + chat.appendChild(document.createElement("div")).textContent = + "Connection closed"; +}; + +messageInput.addEventListener("keydown", (event) => { + if (event.key === "Enter" && messageInput.value.trim() !== "") { + ws.send(messageInput.value); + messageInput.value = ""; + } +}); diff --git a/sample-apps/ws-postgres/public/index.html b/sample-apps/ws-postgres/public/index.html new file mode 100644 index 000000000..ef8d726d4 --- /dev/null +++ b/sample-apps/ws-postgres/public/index.html @@ -0,0 +1,49 @@ + + + + + + Chat App + + + +
+ +

+ + Enter Bye'); DELETE FROM messages;-- to delete all + messages. Reload the page after it to test. +

+ + +