-
Notifications
You must be signed in to change notification settings - Fork 11
Support h3 #572
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
Open
timokoessler
wants to merge
13
commits into
main
Choose a base branch
from
h3
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Support h3 #572
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
a0b8fbf
Start working on H3 support (WIP)
timokoessler 2a34b71
Prepare body parsing
timokoessler d1d01e5
Wrap H3 middleware (WIP)
timokoessler d388cc8
Add e2e test, fix wrapping of from methods
timokoessler 8934fcf
Add H3 middleware, test fixes
timokoessler cfe8a4b
Update library/sources/H3.ts
timokoessler 4fd5aa7
Add docs, fix tests, apply suggestions
timokoessler 073bd75
Merge branch 'main' of github.com:AikidoSec/node-RASP into h3
hansott 403ca9d
Regenerate package lock
hansott da8aac6
Improve comment
hansott d6a5d9f
Fix missing kind for wrapExport
hansott 58a523d
Use `Map` instead of object
hansott 1670d1d
Update sample-apps/h3-postgres/README.md
timokoessler File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"], | ||
}); | ||
} | ||
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
export function formDataToPlainObject(formData: FormData) { | ||
const object: Map<string, unknown> = 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); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Not sure why this is added? Can't find this in h3 codebase?
Wouldn't it be okay to require something from h3 so that we don't have to do this?
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.
Only on mobile right now, but it was added in H3 codebase. Importing is always a bit problematic with bundlers, no?