diff --git a/README.md b/README.md index a7a0d9cce..4f0169db9 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Zen for Node.js 16+ is compatible with: * ✅ [Fastify](docs/fastify.md) 4.x and 5.x * ✅ [Koa](docs/koa.md) 3.x and 2.x * ✅ [NestJS](docs/nestjs.md) 10.x and 11.x +* ✅ [H3](docs/h3.md) 1.8.x and newer 1.x versions ### Database drivers diff --git a/docs/h3.md b/docs/h3.md new file mode 100644 index 000000000..d536a3fbb --- /dev/null +++ b/docs/h3.md @@ -0,0 +1,88 @@ +# H3 + +💡 H3 runs on more JavaScript runtimes than just Node.js. Right now, Zen only supports Node.js. Using `npx listhen` is currently not supported + +At the very beginning of your app.js file, add the following line: + +```js +require("@aikidosec/firewall"); // <-- Include this before any other code or imports + +const { createApp, toNodeListener } = require("h3"); +const { createServer } = require("http"); + +const app = createApp(); + +... + +// Using `npx listhen` is currently not supported +createServer(toNodeListener(app)).listen(process.env.PORT || 3000); + +... + +// ... +``` + +or ESM import style: + +```js +import "@aikidosec/firewall"; + +// ... +``` + +## Blocking mode + +By default, the firewall will run in non-blocking mode. When it detects an attack, the attack will be reported to Aikido if the environment variable `AIKIDO_TOKEN` is set and continue executing the call. + +You can enable blocking mode by setting the environment variable `AIKIDO_BLOCK` to `true`: + +```sh +AIKIDO_BLOCK=true node app.js +``` + +It's recommended to enable this on your staging environment for a considerable amount of time before enabling it on your production environment (e.g. one week). + +## Rate limiting and user blocking + +If you want to add the rate limiting feature to your app, modify your code like this: + +```js +const Zen = require("@aikidosec/firewall"); + +const app = createApp(); + +// Optional, if you want to use user based rate limiting or block specific users +app.use(defineEventHandler((event) => { + // Get the user from your authentication middleware + // or wherever you store the user + Zen.setUser({ + id: "123", + name: "John Doe", // Optional + }); + +})); + +// Call this as early as possible, before other middleware +Zen.addH3Middleware(app); + +app.get(...); +``` + +## Debug mode + +If you need to debug the firewall, you can run your express app with the environment variable `AIKIDO_DEBUG` set to `true`: + +```sh +AIKIDO_DEBUG=true node app.js +``` + +This will output debug information to the console (e.g. if the agent failed to start, no token was found, unsupported packages, ...). + +## Preventing prototype pollution + +Zen can also protect your application against prototype pollution attacks. + +Read [Protect against prototype pollution](./prototype-pollution.md) to learn how to set it up. + +That's it! Your app is now protected by Zen. +If you want to see a full example, check our [h3 sample app](../sample-apps/h3-postgres). diff --git a/end2end/tests/h3-postgres.test.js b/end2end/tests/h3-postgres.test.js new file mode 100644 index 000000000..dc5f9cda6 --- /dev/null +++ b/end2end/tests/h3-postgres.test.js @@ -0,0 +1,111 @@ +const t = require("tap"); +const { spawn } = require("child_process"); +const { resolve } = require("path"); +const timeout = require("../timeout"); + +const pathToApp = resolve(__dirname, "../../sample-apps/h3-postgres", "app.js"); + +t.test("it blocks in blocking mode", (t) => { + const server = spawn(`node`, [pathToApp], { + env: { + ...process.env, + AIKIDO_DEBUG: "true", + AIKIDO_BLOCKING: "true", + PORT: "4000", + }, + }); + + 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(4000) + .then(() => { + return Promise.all([ + fetch( + `http://127.0.0.1:4000/?petname=${encodeURIComponent("Njuska'); DELETE FROM cats_3;-- H")}`, + { + signal: AbortSignal.timeout(5000), + } + ), + fetch("http://127.0.0.1:4000/?petname=Njuska", { + signal: AbortSignal.timeout(5000), + }), + ]); + }) + .then(([sqlInjection, normalSearch]) => { + t.equal(sqlInjection.status, 500); + t.equal(normalSearch.status, 200); + t.match(stdout, /Starting agent/); + t.match(stdout, /Zen has blocked an SQL injection/); + }) + .catch((error) => { + t.fail(error.message); + }) + .finally(() => { + server.kill(); + }); +}); + +t.test("it does not block in dry mode", (t) => { + const server = spawn(`node`, [pathToApp], { + env: { ...process.env, AIKIDO_DEBUG: "true", PORT: "4001" }, + }); + + server.on("close", () => { + t.end(); + }); + + 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(4000) + .then(() => + Promise.all([ + fetch( + `http://127.0.0.1:4001/?petname=${encodeURIComponent("Njuska'); DELETE FROM cats_3;-- H")}`, + { + signal: AbortSignal.timeout(5000), + } + ), + fetch("http://127.0.0.1:4001/?petname=Njuska", { + signal: AbortSignal.timeout(5000), + }), + ]) + ) + .then(([sqlInjection, normalSearch]) => { + t.equal(sqlInjection.status, 200); + t.equal(normalSearch.status, 200); + t.match(stdout, /Starting agent/); + t.notMatch(stdout, /Zen has blocked an SQL injection/); + }) + .catch((error) => { + t.fail(error.message); + }) + .finally(() => { + server.kill(); + }); +}); diff --git a/library/agent/protect.ts b/library/agent/protect.ts index a84259695..6ad7614c2 100644 --- a/library/agent/protect.ts +++ b/library/agent/protect.ts @@ -48,6 +48,7 @@ import { Fastify } from "../sources/Fastify"; import { Koa } from "../sources/Koa"; import { ClickHouse } from "../sinks/ClickHouse"; import { Prisma } from "../sinks/Prisma"; +import { H3 } from "../sources/H3"; function getLogger(): Logger { if (isDebugging()) { @@ -141,6 +142,7 @@ export function getWrappers() { new ClickHouse(), new Prisma(), // new Function(), Disabled because functionName.constructor === Function is false after patching global + new H3(), ]; } diff --git a/library/helpers/formDataToPlainObject.test.ts b/library/helpers/formDataToPlainObject.test.ts new file mode 100644 index 000000000..f078e6844 --- /dev/null +++ b/library/helpers/formDataToPlainObject.test.ts @@ -0,0 +1,69 @@ +import * as t from "tap"; +import { formDataToPlainObject } from "./formDataToPlainObject"; + +t.test( + "simple", + { + skip: !globalThis.FormData + ? "This Node.js version does not support FormData yet" + : false, + }, + async (t) => { + const formData = new FormData(); + formData.append("abc", "123"); + formData.append("another", "42"); + formData.append("hello", "world"); + + t.same(formDataToPlainObject(formData), { + abc: "123", + another: "42", + hello: "world", + }); + } +); + +t.test( + "with arrays", + { + skip: !globalThis.FormData + ? "This Node.js version does not support FormData yet" + : false, + }, + async (t) => { + const formData = new FormData(); + formData.append("abc", "123"); + formData.append("arr", "1"); + formData.append("arr", "2"); + formData.append("arr", "3"); + + t.same(formDataToPlainObject(formData), { + abc: "123", + arr: ["1", "2", "3"], + }); + } +); + +t.test( + "binary data", + { + skip: + !globalThis.FormData || !globalThis.File + ? "This Node.js version does not support FormData or File yet" + : false, + }, + async (t) => { + const formData = new FormData(); + formData.append("abc", "123"); + formData.append("arr", "2"); + formData.append("arr", "3"); + formData.append( + "file", + new File(["hello"], "hello.txt", { type: "text/plain" }) + ); + + t.same(formDataToPlainObject(formData), { + abc: "123", + arr: ["2", "3"], + }); + } +); diff --git a/library/helpers/formDataToPlainObject.ts b/library/helpers/formDataToPlainObject.ts new file mode 100644 index 000000000..f043568df --- /dev/null +++ b/library/helpers/formDataToPlainObject.ts @@ -0,0 +1,27 @@ +export function formDataToPlainObject(formData: FormData) { + const object: Map = new Map(); + formData.forEach((value, key) => { + if (typeof value !== "string") { + return; + } + + if (object.has(key)) { + // If the key already exists, treat it as an array + const entry = object.get(key); + + if (Array.isArray(entry)) { + // If it's already an array, just push the new value + entry.push(value); + return; + } + + // Convert it to an array + object.set(key, [object.get(key), value]); + return; + } + + object.set(key, value); + }); + + return Object.fromEntries(object); +} diff --git a/library/index.ts b/library/index.ts index 6f414d443..ac1baee70 100644 --- a/library/index.ts +++ b/library/index.ts @@ -10,6 +10,7 @@ import { addHapiMiddleware } from "./middleware/hapi"; import { addFastifyHook } from "./middleware/fastify"; import { addKoaMiddleware } from "./middleware/koa"; import { isESM } from "./helpers/isESM"; +import { addH3Middleware } from "./middleware/h3"; const supported = isFirewallSupported(); const shouldEnable = shouldEnableFirewall(); @@ -33,6 +34,7 @@ export { addHapiMiddleware, addFastifyHook, addKoaMiddleware, + addH3Middleware, }; // Required for ESM / TypeScript default export support @@ -46,4 +48,5 @@ export default { addHapiMiddleware, addFastifyHook, addKoaMiddleware, + addH3Middleware, }; diff --git a/library/middleware/h3.ts b/library/middleware/h3.ts new file mode 100644 index 000000000..f4f3a5367 --- /dev/null +++ b/library/middleware/h3.ts @@ -0,0 +1,40 @@ +import { shouldBlockRequest } from "./shouldBlockRequest"; +/** TS_EXPECT_TYPES_ERROR_OPTIONAL_DEPENDENCY **/ +import type { H3Event, App } from "h3"; +import { escapeHTML } from "../helpers/escapeHTML"; + +/** + * Calling this function will setup rate limiting and user blocking for the provided H3 app. + * Attacks will still be blocked by Zen if you do not call this function. + * Execute this function as early as possible in your H3 app, but after the middleware that sets the user. + */ +export function addH3Middleware(app: App) { + const handler = function zenMiddleware(event: H3Event) { + const result = shouldBlockRequest(); + + if (result.block) { + if (result.type === "ratelimited") { + let message = "You are rate limited by Zen."; + if (result.trigger === "ip" && result.ip) { + message += ` (Your IP: ${escapeHTML(result.ip)})`; + } + + event.node.res.statusCode = 429; + event.node.res.setHeader("content-type", "text/plain"); + return message; + } + + if (result.type === "blocked") { + event.node.res.statusCode = 403; + event.node.res.setHeader("content-type", "text/plain"); + return "You are blocked by Zen."; + } + } + }; + + // eslint-disable-next-line camelcase + handler.__is_handler__ = true; + + // @ts-expect-error Ignore + app.use(handler); +} diff --git a/library/package-lock.json b/library/package-lock.json index de626e909..e96ad0f24 100644 --- a/library/package-lock.json +++ b/library/package-lock.json @@ -55,6 +55,7 @@ "follow-redirects": "^1.15.6", "globals": "^16.0.0", "graphql": "^16.8.2", + "h3": "^1.15.1", "hono": "^4.4.2", "koa-router": "^12.0.1", "koa-v2": "npm:koa@^2.16.1", @@ -7420,6 +7421,12 @@ "node": ">= 0.6" } }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "dev": true + }, "node_modules/cookie-parser": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", @@ -7507,6 +7514,15 @@ "node": ">= 8" } }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "dev": true, + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -7647,6 +7663,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7694,6 +7716,12 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -10238,6 +10266,23 @@ "node": ">=14.0.0" } }, + "node_modules/h3": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.3.tgz", + "integrity": "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==", + "dev": true, + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": "^0.3.4", + "defu": "^6.1.4", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.0", + "radix3": "^1.1.2", + "ufo": "^1.6.1", + "uncrypto": "^0.1.3" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -10908,6 +10953,15 @@ "node": ">= 10" } }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, "node_modules/is-actual-promise": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-actual-promise/-/is-actual-promise-1.0.2.tgz", @@ -13533,6 +13587,12 @@ "node": ">= 10.12.0" } }, + "node_modules/node-mock-http": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.0.tgz", + "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", + "dev": true + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -15279,6 +15339,12 @@ "dev": true, "license": "MIT" }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "dev": true + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -18329,6 +18395,12 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -18348,6 +18420,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "dev": true + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/library/package.json b/library/package.json index 4d27bdb7b..8571f789b 100644 --- a/library/package.json +++ b/library/package.json @@ -88,6 +88,7 @@ "follow-redirects": "^1.15.6", "globals": "^16.0.0", "graphql": "^16.8.2", + "h3": "^1.15.1", "hono": "^4.4.2", "koa-router": "^12.0.1", "koa-v2": "npm:koa@^2.16.1", diff --git a/library/sources/H3.test.ts b/library/sources/H3.test.ts new file mode 100644 index 000000000..1158f916d --- /dev/null +++ b/library/sources/H3.test.ts @@ -0,0 +1,581 @@ +import * as t from "tap"; +import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting"; +import { Token } from "../agent/api/Token"; +import { H3 } from "./H3"; +import { HTTPServer } from "./HTTPServer"; +import { getMajorNodeVersion } from "../helpers/getNodeVersion"; +import { isLocalhostIP } from "../helpers/isLocalhostIP"; +import { createTestAgent } from "../helpers/createTestAgent"; +import { getContext } from "../agent/Context"; +import type { IncomingMessage, ServerResponse } from "http"; +import { addH3Middleware } from "../middleware/h3"; +import { setUser } from "../agent/context/user"; + +const agent = createTestAgent({ + token: new Token("123"), + api: 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", "123.1.2.0/24"], + }), +}); +agent.start([new H3(), new HTTPServer()]); + +t.test( + "it works ", + { + skip: + getMajorNodeVersion() < 20 ? "h3 does not work on node < 20" : undefined, + }, + async (t) => { + const { + createApp, + defineEventHandler, + toNodeListener, + createRouter, + readBody, + readFormData, + readMultipartFormData, + readRawBody, + readValidatedBody, + fromNodeMiddleware, + fromWebHandler, + eventHandler, + } = require("h3") as typeof import("h3"); + + const { createServer } = require("http") as typeof import("http"); + + const app = createApp(); + + const router = createRouter(); + + app.use( + defineEventHandler((event) => { + if (event.path === "/block-user") { + setUser({ + id: "567", + }); + } + }) + ); + + addH3Middleware(app); + + router.get( + "/context", + defineEventHandler(() => { + return getContext(); + }) + ); + + router.get( + "/context/:name", + defineEventHandler((event) => { + return getContext(); + }) + ); + + router.add( + "/context2", + defineEventHandler({ + onRequest: [], + onBeforeResponse: [], + handler: (event) => { + return getContext(); + }, + }) + ); + + router.post( + "/post-json", + defineEventHandler(async (event) => { + const body = await readBody(event); + + return { + context: getContext(), + body, + }; + }) + ); + + router.post( + "/post-form-data", + eventHandler(async (event) => { + const body = await readFormData(event); + return { + context: getContext(), + body: body.getAll("arr"), + }; + }) + ); + + router.post( + "/post-multipart-form-data", + defineEventHandler(async (event) => { + const body = await readMultipartFormData(event); + return { + context: getContext(), + body, + }; + }) + ); + + router.post( + "/post-raw-body", + defineEventHandler(async (event) => { + const body = await readRawBody(event); + return { + context: getContext(), + body, + }; + }) + ); + + router.post( + "/post-validated-body", + defineEventHandler(async (event) => { + const body = await readValidatedBody(event, (body) => { + return typeof body === "object" && body !== null; + }); + return { + context: getContext(), + body, + }; + }) + ); + + router.get( + "/from-node-middleware", + fromNodeMiddleware((req: IncomingMessage, res: ServerResponse) => { + res.end(JSON.stringify(getContext())); + }) + ); + + router.get( + "/from-web-handler", + fromWebHandler(async (request: Request) => { + return new Response(JSON.stringify(getContext())); + }) + ); + + app.use( + "/middleware-2", + defineEventHandler({ + onRequest: (event) => { + t.same(getContext()?.source, "h3"); + }, + onBeforeResponse: (event) => { + t.same(getContext()?.source, "h3"); + }, + handler: (event) => { + return getContext(); + }, + }) + ); + + app.use( + "/middleware-3", + defineEventHandler({ + onRequest: [ + async (event) => { + const body = await readBody(event); + t.same(getContext()?.source, "h3"); + }, + (event) => { + t.same(getContext()?.source, "h3"); + }, + ], + onBeforeResponse: [ + (event) => { + t.same(getContext()?.source, "h3"); + }, + ], + handler: (event) => { + return getContext(); + }, + }) + ); + + router.get( + "/rate-limited", + defineEventHandler(() => { + return "Hello world"; + }) + ); + + router.get( + "/block-user", + defineEventHandler(() => { + return "Hello world"; + }) + ); + + app.use(router); + + const server = createServer(toNodeListener(app)); + await new Promise((resolve) => { + server.listen(4123, resolve); + }); + + const app2 = createApp({ + onRequest: (event) => { + t.same(getContext()?.source, "h3"); + }, + onAfterResponse: (event, response) => { + t.same(getContext()?.source, "h3"); + }, + onBeforeResponse: (event, response) => { + t.same(getContext()?.source, "h3"); + }, + onError: (event, error) => { + t.same(getContext()?.source, "h3"); + }, + }); + + app2.use(router); + + const server2 = createServer(toNodeListener(app2)); + await new Promise((resolve) => { + server2.listen(4124, resolve); + }); + + { + const response = await fetch("http://localhost:4123/context?abc=123"); + const body = await response.json(); + t.match(body, { + method: "GET", + url: "/context", + headers: { + host: "localhost:4123", + connection: "keep-alive", + accept: "*/*", + "accept-language": "*", + "sec-fetch-mode": "cors", + "user-agent": "node", + "accept-encoding": "gzip, deflate", + }, + route: "/context", + query: { + abc: "123", + }, + source: "h3", + routeParams: {}, + cookies: {}, + }); + t.ok(isLocalhostIP(body.remoteAddress)); + } + + { + const response = await fetch("http://localhost:4123/context/test", { + headers: { + cookie: "abc=123", + }, + }); + const body = await response.json(); + t.match(body, { + method: "GET", + url: "/context/test", + headers: { + host: "localhost:4123", + connection: "keep-alive", + accept: "*/*", + }, + route: "/context/test", + query: {}, + source: "h3", + routeParams: { + name: "test", + }, + cookies: { + abc: "123", + }, + }); + } + + { + const response = await fetch("http://localhost:4123/context2"); + const body = await response.json(); + t.match(body, { + method: "GET", + url: "/context2", + headers: { + host: "localhost:4123", + connection: "keep-alive", + accept: "*/*", + "accept-language": "*", + "sec-fetch-mode": "cors", + "user-agent": "node", + "accept-encoding": "gzip, deflate", + }, + route: "/context2", + query: {}, + source: "h3", + routeParams: {}, + cookies: {}, + }); + } + + { + const response = await fetch("http://localhost:4123/post-json", { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ abc: "123", arr: [1, 2, 3] }), + }); + + const body = await response.json(); + t.match(body, { + context: { + method: "POST", + url: "/post-json", + headers: { + "content-type": "application/json", + }, + body: { + abc: "123", + arr: [1, 2, 3], + }, + }, + body: { + abc: "123", + arr: [1, 2, 3], + }, + }); + } + + { + const response = await fetch("http://localhost:4123/post-form-data", { + method: "POST", + body: "abc=123&arr=1&arr=2&arr=3", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + }); + const body = await response.json(); + t.match(body, { + context: { + method: "POST", + url: "/post-form-data", + body: { + abc: "123", + arr: ["1", "2", "3"], + }, + }, + body: ["1", "2", "3"], + }); + } + + { + const formData = new FormData(); + formData.append("abc", "123"); + formData.append("arr", "1"); + formData.append("arr", "2"); + formData.append("arr", "3"); + const response = await fetch( + "http://localhost:4123/post-multipart-form-data", + { + method: "POST", + body: formData, + headers: {}, + } + ); + const body = await response.json(); + t.match(body, { + context: { + method: "POST", + url: "/post-multipart-form-data", + body: [ + { + name: "abc", + data: { + type: "Buffer", + }, + }, + { + name: "arr", + data: { + type: "Buffer", + }, + }, + ], + }, + body: [ + { + name: "abc", + data: { + type: "Buffer", + }, + }, + { + name: "arr", + data: { + type: "Buffer", + }, + }, + ], + }); + } + + { + const response = await fetch("http://localhost:4123/post-raw-body", { + method: "POST", + body: "Hello world", + headers: { + "content-type": "text/plain", + }, + }); + const body = await response.json(); + t.match(body, { + context: { + method: "POST", + url: "/post-raw-body", + body: "Hello world", + }, + body: "Hello world", + }); + } + + { + const response = await fetch( + "http://localhost:4123/post-validated-body", + { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ abc: "123", arr: [1, 2, 3] }), + } + ); + + const body = await response.json(); + t.match(body, { + context: { + method: "POST", + url: "/post-validated-body", + headers: { + "content-type": "application/json", + }, + body: { + abc: "123", + arr: [1, 2, 3], + }, + }, + body: { + abc: "123", + arr: [1, 2, 3], + }, + }); + } + + { + const response = await fetch( + "http://localhost:4123/from-node-middleware?abc=123" + ); + const body = await response.json(); + t.match(body, { + method: "GET", + url: "/from-node-middleware", + source: "h3", + query: { + abc: "123", + }, + }); + } + + { + const response = await fetch("http://localhost:4123/middleware-2"); + const body = await response.json(); + t.match(body, { + method: "GET", + url: "/middleware-2", + source: "h3", + query: {}, + }); + } + + { + const response = await fetch("http://localhost:4123/middleware-3", { + method: "POST", + body: JSON.stringify({ abc: "123", arr: [1, 2, 3] }), + headers: { + "content-type": "application/json", + }, + }); + const body = await response.json(); + t.match(body, { + method: "POST", + url: "/middleware-3", + source: "h3", + query: {}, + body: { + abc: "123", + arr: [1, 2, 3], + }, + }); + } + + { + const response = await fetch( + "http://localhost:4124/from-node-middleware?abc=123" + ); + const body = await response.json(); + t.match(body, { + method: "GET", + url: "/from-node-middleware", + source: "h3", + query: { + abc: "123", + }, + }); + } + + { + const response = await fetch("http://localhost:4123/from-web-handler"); + const body = await response.json(); + t.match(body, { + method: "GET", + url: "/from-web-handler", + source: "h3", + query: {}, + }); + } + + { + const response = await fetch("http://localhost:4123/rate-limited"); + t.equal(response.status, 200); + + const response2 = await fetch("http://localhost:4123/rate-limited"); + t.equal(response2.status, 200); + + const response3 = await fetch("http://localhost:4123/rate-limited"); + t.equal(response3.status, 429); + const text = await response3.text(); + t.match(text, /You are rate limited by Zen./); + } + + { + const response = await fetch("http://localhost:4123/block-user"); + t.equal(response.status, 403); + const text = await response.text(); + t.equal(text, "You are blocked by Zen."); + } + + server.close(); + server2.close(); + } +); diff --git a/library/sources/H3.ts b/library/sources/H3.ts new file mode 100644 index 000000000..039c8e7ba --- /dev/null +++ b/library/sources/H3.ts @@ -0,0 +1,123 @@ +import { Hooks } from "../agent/hooks/Hooks"; +import { Wrapper } from "../agent/Wrapper"; +import { wrapExport } from "../agent/hooks/wrapExport"; +import { wrapEventHandler } from "./h3/wrapEventHandler"; +import type { EventHandler } from "h3"; +import { wrapReadBody } from "./h3/wrapReadBody"; +import { type H3Middleware, wrapMiddleware } from "./h3/wrapMiddleware"; +import { isPlainObject } from "../helpers/isPlainObject"; + +export class H3 implements Wrapper { + private wrapEventHandler(args: unknown[], h3: typeof import("h3")) { + if (args.length === 0) { + return args; + } + + if (typeof args[0] === "function") { + return [wrapEventHandler(args[0] as EventHandler, h3)]; + } + + if (isPlainObject(args[0])) { + const config = args[0] as { [key: string]: unknown }; + + if ("handler" in config && typeof config.handler === "function") { + config.handler = wrapEventHandler(config.handler as EventHandler, h3); + } + + const middlewareFuncs = ["onRequest", "onBeforeResponse"]; + for (const func of middlewareFuncs) { + if (func in config) { + // Can be a function or an array of functions + if (typeof config[func] === "function") { + config[func] = wrapMiddleware(config[func] as H3Middleware, h3); + } else if (Array.isArray(config[func])) { + config[func] = (config[func] as H3Middleware[]).map((m) => + wrapMiddleware(m, h3) + ); + } + } + } + } + + return args; + } + + private wrapCreateApp(args: unknown[], h3: typeof import("h3")) { + if (args.length === 0 || !isPlainObject(args[0])) { + return args; + } + + const config = args[0] as { [key: string]: unknown }; + const funcs = [ + "onRequest", + "onBeforeResponse", + "onAfterResponse", + "onError", + ]; + + for (const func of funcs) { + if (func in config && typeof config[func] === "function") { + config[func] = wrapMiddleware(config[func] as H3Middleware, h3); + } + } + + return args; + } + + private wrapFromFunction(returnValue: unknown, h3: typeof import("h3")) { + if (typeof returnValue === "function") { + return wrapEventHandler(returnValue as EventHandler, h3); + } + + return returnValue; + } + + wrap(hooks: Hooks) { + hooks + .addPackage("h3") + .withVersion("^1.8.0") + .onRequire((exports, pkgInfo) => { + wrapExport(exports, "defineEventHandler", pkgInfo, { + kind: undefined, + modifyArgs: (args) => { + return this.wrapEventHandler(args, exports); + }, + }); + + wrapExport(exports, "createApp", pkgInfo, { + kind: undefined, + modifyArgs: (args) => { + return this.wrapCreateApp(args, exports); + }, + }); + + wrapExport(exports, "fromNodeMiddleware", pkgInfo, { + kind: undefined, + modifyReturnValue: (_args, returnValue) => { + return this.wrapFromFunction(returnValue, exports); + }, + }); + + wrapExport(exports, "fromWebHandler", pkgInfo, { + kind: undefined, + modifyReturnValue: (_args, returnValue) => { + return this.wrapFromFunction(returnValue, exports); + }, + }); + + const bodyFuncs = [ + "readBody", + "readFormData", + "readMultipartFormData", + "readRawBody", + "readValidatedBody", + ]; + for (const func of bodyFuncs) { + wrapExport(exports, func, pkgInfo, { + kind: undefined, + modifyReturnValue: wrapReadBody, + }); + } + }); + } +} diff --git a/library/sources/h3/contextFromEvent.ts b/library/sources/h3/contextFromEvent.ts new file mode 100644 index 000000000..ff6123430 --- /dev/null +++ b/library/sources/h3/contextFromEvent.ts @@ -0,0 +1,37 @@ +import { Context, getContext } from "../../agent/Context"; +import { buildRouteFromURL } from "../../helpers/buildRouteFromURL"; +import { getIPAddressFromRequest } from "../../helpers/getIPAddressFromRequest"; +import type { H3Event } from "h3"; + +export async function contextFromEvent( + event: H3Event, + h3: typeof import("h3") +): Promise { + const existingContext = getContext(); + + const headers = h3.getHeaders(event); + + const url = h3.getRequestURL(event).toString(); + + return { + method: event.method, + remoteAddress: getIPAddressFromRequest({ + headers: headers, + remoteAddress: h3.getRequestIP(event, { + xForwardedFor: false, + }), + }), + // Pass the body from the existing context if it's already set, otherwise the body is set in wrapRequestBodyParsing + body: + existingContext && existingContext.source === "h3" + ? existingContext.body + : undefined, + url: url, + headers: headers, + routeParams: h3.getRouterParams(event), + query: h3.getQuery(event), + cookies: h3.parseCookies(event), + source: "h3", + route: buildRouteFromURL(url), + }; +} diff --git a/library/sources/h3/wrapEventHandler.ts b/library/sources/h3/wrapEventHandler.ts new file mode 100644 index 000000000..55e353517 --- /dev/null +++ b/library/sources/h3/wrapEventHandler.ts @@ -0,0 +1,19 @@ +import type { EventHandler, H3Event } from "h3"; +import { runWithContext } from "../../agent/Context"; +import { contextFromEvent } from "./contextFromEvent"; +import { createWrappedFunction } from "../../helpers/wrap"; + +export function wrapEventHandler( + handler: EventHandler, + h3: typeof import("h3") +): EventHandler { + return createWrappedFunction(handler, (handler) => { + return async (event: H3Event) => { + const context = await contextFromEvent(event, h3); + + return await runWithContext(context, async () => { + return await handler(event); + }); + }; + }) as EventHandler; +} diff --git a/library/sources/h3/wrapMiddleware.ts b/library/sources/h3/wrapMiddleware.ts new file mode 100644 index 000000000..92e031ed3 --- /dev/null +++ b/library/sources/h3/wrapMiddleware.ts @@ -0,0 +1,41 @@ +import type { H3Event } from "h3"; +import { runWithContext } from "../../agent/Context"; +import { contextFromEvent } from "./contextFromEvent"; +import { createWrappedFunction } from "../../helpers/wrap"; + +export type H3Middleware = (...args: unknown[]) => void | Promise; + +export function wrapMiddleware( + middleware: H3Middleware, + h3: typeof import("h3") +): H3Middleware { + return createWrappedFunction(middleware, (middleware) => { + return async (...args: unknown[]) => { + const event = getEventFromArgs(args); + + if (!event) { + return await middleware(...args); + } + + const context = await contextFromEvent(event, h3); + + return await runWithContext(context, async () => { + return await middleware(...args); + }); + }; + }) as H3Middleware; +} + +function getEventFromArgs(args: unknown[]): H3Event | undefined { + for (const arg of args) { + if ( + arg && + typeof arg === "object" && + "__is_event__" in arg && + arg.__is_event__ + ) { + return arg as H3Event; + } + } + return undefined; +} diff --git a/library/sources/h3/wrapReadBody.ts b/library/sources/h3/wrapReadBody.ts new file mode 100644 index 000000000..e1ecdb607 --- /dev/null +++ b/library/sources/h3/wrapReadBody.ts @@ -0,0 +1,21 @@ +import { getContext, updateContext } from "../../agent/Context"; +import { formDataToPlainObject } from "../../helpers/formDataToPlainObject"; + +export async function wrapReadBody(_args: unknown[], returnValue: unknown) { + const context = getContext(); + + if (!context) { + return returnValue; + } + + const body = await returnValue; + if (body) { + if (body instanceof FormData) { + updateContext(context, "body", formDataToPlainObject(body)); + } else { + updateContext(context, "body", body); + } + } + + return body; +} diff --git a/sample-apps/express-postgres/README.md b/sample-apps/express-postgres/README.md index 6a813a90f..03a2cd941 100644 --- a/sample-apps/express-postgres/README.md +++ b/sample-apps/express-postgres/README.md @@ -8,4 +8,4 @@ Try the following URLs: - http://localhost:4000/ : List all cats - http://localhost:4000/?petname=Kitty : This will add a new cat named "Kitty" -- http://localhost:4000/?petname=Kitty'); DELETE FROM cats;-- H : This will delete all cats +- http://localhost:4000/?petname=Kitty'); DELETE FROM cats_3;-- H : This will delete all cats diff --git a/sample-apps/h3-postgres/Cats.js b/sample-apps/h3-postgres/Cats.js new file mode 100644 index 000000000..47e0fd03b --- /dev/null +++ b/sample-apps/h3-postgres/Cats.js @@ -0,0 +1,22 @@ +class Cats { + constructor(db) { + this.db = db; + } + + async add(name) { + // This is unsafe! This is for demo purposes only, you should use parameterized queries. + await this.db.query(`INSERT INTO cats_3 (petname) VALUES ('${name}');`); + } + + async getAll() { + const cats = await this.db.query("SELECT petname FROM cats_3;"); + + return cats.rows.map((row) => row.petname); + } + + async clear() { + await this.db.query("DELETE FROM cats_3;"); + } +} + +module.exports = Cats; diff --git a/sample-apps/h3-postgres/README.md b/sample-apps/h3-postgres/README.md new file mode 100644 index 000000000..051eeef97 --- /dev/null +++ b/sample-apps/h3-postgres/README.md @@ -0,0 +1,11 @@ +# h3-postgres + +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 `npm run sample-app h3-postgres` to start the server. + +Try the following URLs: + +- http://localhost:4000/ : List all cats +- http://localhost:4000/?petname=Kitty : This will add a new cat named "Kitty" +- http://localhost:4000/?petname=Kitty'); DELETE FROM cats_3;-- H : This will delete all cats diff --git a/sample-apps/h3-postgres/app.js b/sample-apps/h3-postgres/app.js new file mode 100644 index 000000000..81e00de68 --- /dev/null +++ b/sample-apps/h3-postgres/app.js @@ -0,0 +1,91 @@ +const Zen = require("@aikidosec/firewall"); +const { + createApp, + createRouter, + defineEventHandler, + getQuery, + sendRedirect, + toNodeListener, +} = require("h3"); +const { Client } = require("pg"); + +const Cats = require("./Cats"); +const { createServer } = require("http"); + +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 cats_3 ( + petname varchar(255), + comment varchar(255) + ); + `); + + return client; +} + +function getHTMLBody(cats) { + return ` + + +

All cats : ${cats.join(", ")}

+
+ + + +
+ Test injection / Clear table + +`; +} + +(async () => { + const db = await createConnection(); + const cats = new Cats(db); + + // Create an app instance + const app = createApp(); + + Zen.addH3Middleware(app); + + // Create a new router and register it in app + const router = createRouter(); + + // Add a new route that matches GET requests to / path + router.get( + "/", + defineEventHandler(async (event) => { + const query = getQuery(event); + + if (typeof query["petname"] === "string") { + await cats.add(query["petname"]); + } + + return getHTMLBody(await cats.getAll()); + }) + ); + + router.get( + "/clear", + defineEventHandler(async (event) => { + await cats.clear(); + return sendRedirect(event, "/"); + }) + ); + + app.use(router); + + createServer(toNodeListener(app)).listen(process.env.PORT || 3000, () => { + console.log(`Listening on port ${process.env.PORT || 3000}`); + }); +})(); diff --git a/sample-apps/h3-postgres/package-lock.json b/sample-apps/h3-postgres/package-lock.json new file mode 100644 index 000000000..df8eb2f4d --- /dev/null +++ b/sample-apps/h3-postgres/package-lock.json @@ -0,0 +1,367 @@ +{ + "name": "h3-postgres", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "h3": "^1.15.1", + "pg": "^8.14.1" + }, + "devDependencies": { + "@types/pg": "^8.11.11" + } + }, + "../../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/@types/node": { + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.11.11", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz", + "integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.4.tgz", + "integrity": "sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "license": "MIT" + }, + "node_modules/h3": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.1.tgz", + "integrity": "sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": "^0.3.3", + "defu": "^6.1.4", + "destr": "^2.0.3", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.0", + "radix3": "^1.1.2", + "ufo": "^1.5.4", + "uncrypto": "^0.1.3" + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/node-mock-http": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.0.tgz", + "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", + "license": "MIT" + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", + "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.8.0", + "pg-protocol": "^1.8.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-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", + "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", + "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==", + "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/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "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/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "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/h3-postgres/package.json b/sample-apps/h3-postgres/package.json new file mode 100644 index 000000000..bebf9890b --- /dev/null +++ b/sample-apps/h3-postgres/package.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "@aikidosec/firewall": "file:../../build", + "h3": "^1.15.1", + "pg": "^8.14.1" + }, + "type": "commonjs", + "devDependencies": { + "@types/pg": "^8.11.11" + } +}