-
Notifications
You must be signed in to change notification settings - Fork 43
Add ElysiaJS support #1055
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add ElysiaJS support #1055
Changes from all commits
1df4234
0806e36
9c061e5
fe9d727
1c1675d
ee7339c
40fc117
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # ElysiaJS | ||
|
|
||
| π‘ ElysiaJS runs on more JavaScript runtimes than just Node.js. Right now, Zen only supports Node.js. | ||
|
|
||
| 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 { Elysia } = require("elysia"); | ||
|
|
||
| new Elysia({ adapter: node() }).get("/", () => "Hello World").listen(3000); | ||
|
|
||
| // ... | ||
| ``` | ||
|
|
||
| or using `import` syntax: | ||
|
|
||
| ```js | ||
| import "@aikidosec/firewall"; | ||
|
|
||
| // ... | ||
| ``` | ||
|
|
||
| > [!NOTE] | ||
| > Many TypeScript projects use `import` syntax but still compile to CommonJS β in that case, the setup above works as-is. If your app runs as **native ESM** at runtime (e.g. `"type": "module"` in package.json), see [ESM setup](./esm.md) for additional steps. | ||
|
|
||
| ## 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"); | ||
|
|
||
| new Elysia({ adapter: node() }) | ||
| .onBeforeHandle(Zen.elysiaHandler) // <-- Add this line | ||
| .get("/", () => "Hello World") | ||
| .listen(3000); | ||
| ``` | ||
|
|
||
| ## Debug mode | ||
|
|
||
| If you need to debug the firewall, you can run your ElysiaJS 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 [ElysiaJS sample app](../sample-apps/elysiajs-pg-esm). | ||
|
|
||
| ## Graceful shutdown | ||
|
|
||
| It is recommended to add a shutdown handler to your app to ensure that no statistics are lost when the app is stopped. You can find more information [here](./graceful-shutdown.md). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| import { spawn } from "child_process"; | ||
| import { resolve } from "path"; | ||
| import { test } from "node:test"; | ||
| import { equal, fail, match, doesNotMatch } from "node:assert"; | ||
| import { getRandomPort } from "./utils/get-port.mjs"; | ||
| import { timeout } from "./utils/timeout.mjs"; | ||
|
|
||
| const pathToAppDir = resolve( | ||
| import.meta.dirname, | ||
| "../../sample-apps/elysiajs-pg-esm" | ||
| ); | ||
|
|
||
| const port = await getRandomPort(); | ||
| const port2 = await getRandomPort(); | ||
|
|
||
| test("it blocks request in blocking mode", async () => { | ||
| const server = spawn( | ||
| `node`, | ||
| ["-r", "@aikidosec/firewall/instrument", "./app.ts"], | ||
| { | ||
| cwd: pathToAppDir, | ||
| env: { | ||
| ...process.env, | ||
| AIKIDO_DEBUG: "true", | ||
| AIKIDO_BLOCK: "true", | ||
| PORT: port.toString(), | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| try { | ||
| server.on("error", (err) => { | ||
| 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 | ||
| await timeout(2000); | ||
|
|
||
| const [sqlInjection, normalAdd] = await Promise.all([ | ||
| fetch(`http://127.0.0.1:${port}/add`, { | ||
| method: "POST", | ||
| body: JSON.stringify({ name: "Njuska'); DELETE FROM cats_7;-- H" }), | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| signal: AbortSignal.timeout(5000), | ||
| }), | ||
| fetch(`http://127.0.0.1:${port}/add`, { | ||
| method: "POST", | ||
| body: JSON.stringify({ name: "Miau" }), | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| signal: AbortSignal.timeout(5000), | ||
| }), | ||
| ]); | ||
|
|
||
| equal(sqlInjection.status, 500); | ||
| equal(normalAdd.status, 200); | ||
| match(stdout, /Starting agent/); | ||
| match(stdout, /Zen has blocked an SQL injection/); | ||
| } catch (err) { | ||
| fail(err); | ||
| } finally { | ||
| server.kill(); | ||
| } | ||
| }); | ||
|
|
||
| test("it does not block request in monitoring mode", async () => { | ||
| const server = spawn( | ||
| `node`, | ||
| ["-r", "@aikidosec/firewall/instrument", "./app.ts", port2], | ||
| { | ||
| cwd: pathToAppDir, | ||
| env: { | ||
| ...process.env, | ||
| AIKIDO_DEBUG: "true", | ||
| AIKIDO_BLOCK: "false", | ||
| PORT: port2.toString(), | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| try { | ||
| server.on("error", (err) => { | ||
| 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 | ||
| await timeout(2000); | ||
|
|
||
| const [sqlInjection, normalAdd] = await Promise.all([ | ||
| fetch(`http://127.0.0.1:${port2}/add`, { | ||
| method: "POST", | ||
| body: JSON.stringify({ | ||
| name: "Njuska'); DELETE FROM cats_7;-- H", | ||
| }), | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| signal: AbortSignal.timeout(5000), | ||
| }), | ||
| fetch(`http://127.0.0.1:${port2}/add`, { | ||
| method: "POST", | ||
| body: JSON.stringify({ name: "Miau" }), | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| signal: AbortSignal.timeout(5000), | ||
| }), | ||
| ]); | ||
|
|
||
| equal(sqlInjection.status, 200); | ||
| equal(normalAdd.status, 200); | ||
| match(stdout, /Starting agent/); | ||
| doesNotMatch(stdout, /Zen has blocked an SQL injection/); | ||
| } catch (err) { | ||
| fail(err); | ||
| } finally { | ||
| server.kill(); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -618,6 +618,7 @@ export class Agent { | |
| "express", | ||
| "fastify", | ||
| "hono", | ||
| "elysia", | ||
| "koa", | ||
| "@hapi/hapi", | ||
| "restify", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { shouldBlockRequest } from "./shouldBlockRequest"; | ||
| import { escapeHTML } from "../helpers/escapeHTML"; | ||
|
|
||
| /** | ||
| * Adding this handler using app.onBeforeHandle(elysiaHandler) will setup rate limiting and user blocking for the provided Elysia app. | ||
| * Attacks will still be blocked by Zen if you do not add this handler. | ||
| */ | ||
| export const elysiaHandler: () => Response | void = () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no handler type we can import from the package? like other middleware?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did this first, but then it does import("elysia") in our
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? :D
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does not work, tested it :D
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We had the same issue with Fastify too, it's because the type is to complex maybe? Not really sure what the difference is.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See failing check: https://github.com/AikidoSec/firewall-node/actions/runs/27363774019/job/80858105374. And fixed in 40fc117 |
||
| 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)})`; | ||
| } | ||
|
|
||
| return new Response(message, { | ||
| status: 429, | ||
| headers: { | ||
| "Retry-After": result.retryAfterSeconds.toString(), | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| if (result.type === "blocked") { | ||
| return new Response("You are blocked by Zen.", { status: 403 }); | ||
| } | ||
| } | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.