Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ jobs:
command: npm run test:esm

- name: Upload coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./library/.tap/report/lcov.info,./.esm-tests/tests/lcov.info
use_oidc: true
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Zen for Node.js 16+ is compatible with:
- βœ… [Koa](docs/koa.md) 3.x and 2.x
- βœ… [NestJS](docs/nestjs.md) 10.x and 11.x
- βœ… [Restify](docs/restify.md) 11.x, 10.x, 9.x and 8.x
- βœ… [ElysiaJS](docs/elysiajs.md) 1.x (minimum 1.4.0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- βœ… [ElysiaJS](docs/elysiajs.md) 1.x (minimum 1.4.0)
- βœ… [ElysiaJS](docs/elysiajs.md) 1.4.x


### Database drivers

Expand Down
74 changes: 74 additions & 0 deletions docs/elysiajs.md
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).
142 changes: 142 additions & 0 deletions end2end/tests-new/elysia-pg-esm.test.mjs
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();
}
});
1 change: 1 addition & 0 deletions library/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ export class Agent {
"express",
"fastify",
"hono",
"elysia",
"koa",
"@hapi/hapi",
"restify",
Expand Down
2 changes: 2 additions & 0 deletions library/agent/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
createCloudFunctionWrapper,
FunctionsFramework,
} from "../sources/FunctionsFramework";
import { Elysia } from "../sources/Elysia";
import { Hono } from "../sources/Hono";
import { HTTPServer } from "../sources/HTTPServer";
import { createLambdaWrapper } from "../sources/Lambda";
Expand Down Expand Up @@ -151,6 +152,7 @@ export function getWrappers() {
new Path(),
new HTTPServer(),
new Hono(),
new Elysia(),
new GraphQL(),
new OpenAI(),
new Mistral(),
Expand Down
3 changes: 3 additions & 0 deletions library/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { colorText } from "./helpers/colorText";
import { warnBox } from "./helpers/warnBox";
import { isPreloaded } from "./helpers/isPreloaded";
import { warnIfEntrypointIsModule } from "./helpers/warnIfEntrypointIsModule";
import { elysiaHandler } from "./middleware/elysia";

// Prevent logging twice / trying to start agent twice
if (!isNewHookSystemUsed()) {
Expand Down Expand Up @@ -73,6 +74,7 @@ export {
fastifyHook,
addKoaMiddleware,
addRestifyMiddleware,
elysiaHandler,
setRateLimitGroup,
shutdown,
setTenantId,
Expand All @@ -93,6 +95,7 @@ export default {
fastifyHook,
addKoaMiddleware,
addRestifyMiddleware,
elysiaHandler,
setRateLimitGroup,
shutdown,
setTenantId,
Expand Down
30 changes: 30 additions & 0 deletions library/middleware/elysia.ts
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 = () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no handler type we can import from the package? like other middleware?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this first, but then it does import("elysia") in our index.d.ts file and breaks compilation of TypeScript projects using our library πŸ˜…

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/** TS_EXPECT_TYPES_ERROR_OPTIONAL_DEPENDENCY **/
import type { Express, Router } from "express";

? :D

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not work, tested it :D

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 });
}
}
};
Loading