Skip to content

Commit 37b6b1a

Browse files
authored
Merge pull request #362 from AikidoSec/koa
Add support for koa
2 parents 8c04e56 + 315a333 commit 37b6b1a

25 files changed

+4095
-2
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ nestjs-sentry:
7272
fastify-mysql2:
7373
cd sample-apps/fastify-mysql2 && AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node --preserve-symlinks app.js
7474

75+
.PHONY: koa-sqlite3
76+
koa-sqlite3:
77+
cd sample-apps/koa-sqlite3 && AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node --preserve-symlinks app.js
78+
7579
.PHONY: install
7680
install:
7781
mkdir -p build

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Zen for Node.js 16+ is compatible with:
4444
*[micro](docs/micro.md) 10.x
4545
*[Next.js](docs/next.md) 12.x, 13.x and 14.x
4646
*[Fastify](docs/fastify.md) 4.x and 5.x
47+
*[Koa](docs/koa.md) 2.x
4748

4849
### Database drivers
4950

@@ -89,6 +90,11 @@ See list above for supported database drivers.
8990

9091
*[`ShellJS`](https://www.npmjs.com/package/shelljs) 0.8.x, 0.7.x
9192

93+
### Routers
94+
95+
*[`@koa/router`](https://www.npmjs.com/package/@koa/router) 13.x, 12.x, 11.x and 10.x
96+
97+
9298
## Installation
9399

94100
We recommend testing Zen locally or on staging before deploying to production.

docs/koa.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Koa
2+
3+
At the very beginning of your app.js file, add the following line:
4+
5+
```js
6+
require('@aikidosec/firewall'); // <-- Include this before any other code or imports
7+
8+
const Koa = require("koa");
9+
10+
const app = Koa();
11+
12+
app.use(...);
13+
14+
// ...
15+
```
16+
17+
or ESM import style:
18+
19+
```js
20+
import "@aikidosec/firewall";
21+
22+
// ...
23+
```
24+
25+
Zen also supports `@koa/router` or `koa-router`.
26+
27+
## Blocking mode
28+
29+
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.
30+
31+
You can enable blocking mode by setting the environment variable `AIKIDO_BLOCK` to `true`:
32+
33+
```sh
34+
AIKIDO_BLOCK=true node app.js
35+
```
36+
37+
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).
38+
39+
## Rate limiting and user blocking
40+
41+
If you want to add the rate limiting feature to your app, modify your code like this:
42+
43+
```js
44+
const Zen = require("@aikidosec/firewall");
45+
46+
const app = Koa();
47+
48+
// Optional, if you want to use user based rate limiting or block specific users
49+
app.use(async (ctx, next) => {
50+
// Get the user from your authentication middleware
51+
// or wherever you store the user
52+
Zen.setUser({
53+
id: "123",
54+
name: "John Doe", // Optional
55+
});
56+
57+
await next();
58+
});
59+
60+
// Place this middleware after your authentication middleware
61+
// As early as possible in the middleware chain
62+
Zen.addKoaMiddleware(app);
63+
64+
app.get(...);
65+
```
66+
67+
If you are using `@koa/router` or `koa-router`, please make sure to place the `.use(router.routes())` middleware after the Zen middleware:
68+
69+
## Debug mode
70+
71+
If you need to debug the firewall, you can run your express app with the environment variable `AIKIDO_DEBUG` set to `true`:
72+
73+
```sh
74+
AIKIDO_DEBUG=true node app.js
75+
```
76+
77+
This will output debug information to the console (e.g. if the agent failed to start, no token was found, unsupported packages, ...).
78+
79+
## Preventing prototype pollution
80+
81+
Zen can also protect your application against prototype pollution attacks.
82+
83+
Read [Protect against prototype pollution](./prototype-pollution.md) to learn how to set it up.
84+
85+
That's it! Your app is now protected by Zen.
86+
If you want to see a full example, check our [koa sample app](../sample-apps/koa-sqlite3).

end2end/tests/koa-sqlite3.test.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
const t = require("tap");
2+
const { spawn } = require("child_process");
3+
const { resolve } = require("path");
4+
const timeout = require("../timeout");
5+
6+
const pathToApp = resolve(__dirname, "../../sample-apps/koa-sqlite3", "app.js");
7+
8+
t.test("it blocks in blocking mode", (t) => {
9+
const server = spawn(`node`, ["--preserve-symlinks", pathToApp, "4002"], {
10+
env: { ...process.env, AIKIDO_DEBUG: "true", AIKIDO_BLOCKING: "true" },
11+
});
12+
13+
server.on("close", () => {
14+
t.end();
15+
});
16+
17+
server.on("error", (err) => {
18+
t.fail(err.message);
19+
});
20+
21+
let stdout = "";
22+
server.stdout.on("data", (data) => {
23+
stdout += data.toString();
24+
});
25+
26+
let stderr = "";
27+
server.stderr.on("data", (data) => {
28+
stderr += data.toString();
29+
});
30+
31+
// Wait for the server to start
32+
timeout(2000)
33+
.then(() => {
34+
return Promise.all([
35+
fetch("http://127.0.0.1:4002/add", {
36+
method: "POST",
37+
body: JSON.stringify({ name: "Test'), ('Test2');--" }),
38+
headers: {
39+
"Content-Type": "application/json",
40+
},
41+
signal: AbortSignal.timeout(5000),
42+
}),
43+
fetch("http://127.0.0.1:4002/add", {
44+
method: "POST",
45+
body: JSON.stringify({ name: "Miau" }),
46+
headers: {
47+
"Content-Type": "application/json",
48+
},
49+
signal: AbortSignal.timeout(5000),
50+
}),
51+
]);
52+
})
53+
.then(([sqlInjection, normalAdd]) => {
54+
t.equal(sqlInjection.status, 500);
55+
t.equal(normalAdd.status, 200);
56+
t.match(stdout, /Starting agent/);
57+
t.match(stderr, /Zen has blocked an SQL injection/);
58+
})
59+
.catch((error) => {
60+
t.fail(error.message);
61+
})
62+
.finally(() => {
63+
server.kill();
64+
});
65+
});
66+
67+
t.test("it does not block in dry mode", (t) => {
68+
const server = spawn(`node`, ["--preserve-symlinks", pathToApp, "4003"], {
69+
env: { ...process.env, AIKIDO_DEBUG: "true" },
70+
});
71+
72+
server.on("close", () => {
73+
t.end();
74+
});
75+
76+
let stdout = "";
77+
server.stdout.on("data", (data) => {
78+
stdout += data.toString();
79+
});
80+
81+
let stderr = "";
82+
server.stderr.on("data", (data) => {
83+
stderr += data.toString();
84+
});
85+
86+
// Wait for the server to start
87+
timeout(2000)
88+
.then(() =>
89+
Promise.all([
90+
fetch("http://127.0.0.1:4003/add", {
91+
method: "POST",
92+
body: JSON.stringify({ name: "Test'), ('Test2');--" }),
93+
headers: {
94+
"Content-Type": "application/json",
95+
},
96+
signal: AbortSignal.timeout(5000),
97+
}),
98+
fetch("http://127.0.0.1:4003/add", {
99+
method: "POST",
100+
body: JSON.stringify({ name: "Miau" }),
101+
headers: {
102+
"Content-Type": "application/json",
103+
},
104+
signal: AbortSignal.timeout(5000),
105+
}),
106+
])
107+
)
108+
.then(([sqlInjection, normalAdd]) => {
109+
t.equal(sqlInjection.status, 200);
110+
t.equal(normalAdd.status, 200);
111+
t.match(stdout, /Starting agent/);
112+
t.notMatch(stderr, /Zen has blocked an SQL injection/);
113+
})
114+
.catch((error) => {
115+
t.fail(error.message);
116+
})
117+
.finally(() => {
118+
server.kill();
119+
});
120+
});

library/agent/protect.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import { isDebugging } from "../helpers/isDebugging";
4545
import { shouldBlock } from "../helpers/shouldBlock";
4646
import { Postgresjs } from "../sinks/Postgresjs";
4747
import { Fastify } from "../sources/Fastify";
48+
import { Koa } from "../sources/Koa";
49+
import { KoaRouter } from "../sources/KoaRouter";
4850

4951
function getLogger(): Logger {
5052
if (isDebugging()) {
@@ -132,6 +134,8 @@ function getWrappers() {
132134
new BetterSQLite3(),
133135
new Postgresjs(),
134136
new Fastify(),
137+
new Koa(),
138+
new KoaRouter(),
135139
];
136140
}
137141

library/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { addExpressMiddleware } from "./middleware/express";
77
import { addHonoMiddleware } from "./middleware/hono";
88
import { addHapiMiddleware } from "./middleware/hapi";
99
import { addFastifyHook } from "./middleware/fastify";
10+
import { addKoaMiddleware } from "./middleware/koa";
1011

1112
const supported = isFirewallSupported();
1213
const shouldEnable = shouldEnableFirewall();
@@ -22,4 +23,5 @@ export {
2223
addHonoMiddleware,
2324
addHapiMiddleware,
2425
addFastifyHook,
26+
addKoaMiddleware,
2527
};

library/middleware/koa.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { shouldBlockRequest } from "./shouldBlockRequest";
2+
import { escapeHTML } from "../helpers/escapeHTML";
3+
import type * as Application from "koa";
4+
5+
/**
6+
* Calling this function will setup rate limiting and user blocking for the provided Express app.
7+
* Attacks will still be blocked by Zen if you do not call this function.
8+
* Execute this function as early as possible in your Express app, but after the middleware that sets the user.
9+
*/
10+
export function addKoaMiddleware(app: Application): void {
11+
app.use(async (ctx, next) => {
12+
const result = shouldBlockRequest();
13+
14+
if (result.block) {
15+
if (result.type === "ratelimited") {
16+
let message = "You are rate limited by Zen.";
17+
if (result.trigger === "ip" && result.ip) {
18+
message += ` (Your IP: ${escapeHTML(result.ip)})`;
19+
}
20+
21+
ctx.type = "text/plain";
22+
ctx.body = message;
23+
ctx.status = 429;
24+
return;
25+
}
26+
27+
if (result.type === "blocked") {
28+
ctx.type = "text/plain";
29+
ctx.body = "You are blocked by Zen.";
30+
ctx.status = 403;
31+
return;
32+
}
33+
}
34+
35+
await next();
36+
});
37+
}

0 commit comments

Comments
 (0)