-
Notifications
You must be signed in to change notification settings - Fork 41
Support Loopback 4 #862
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
10
commits into
main
Choose a base branch
from
loopback4
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 Loopback 4 #862
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
78d8507
Create loopback sample app
timokoessler dbcd815
Merge branch 'main' into loopback4
timokoessler 7b6732f
Instrument loopback body parsing
timokoessler 058f205
Add docs and tests
timokoessler bc426cc
Support route parameters
timokoessler 67b2e12
Merge branch 'main' into loopback4
timokoessler 21c1c56
Fix format and esm tests
timokoessler cc7e2f7
Simplify if
timokoessler 4844663
Fix e2e tests
timokoessler cdfec1e
Add more tests
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,97 @@ | ||
| # LoopBack 4 | ||
|
|
||
| At the very beginning of your `index.ts` file, add the following line: | ||
|
|
||
| ```js | ||
| import "@aikidosec/firewall"; // <-- Include this before any other code or imports | ||
|
|
||
| // ... | ||
| ``` | ||
|
|
||
| > [!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 npm start | ||
| ``` | ||
|
|
||
| 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, you can create a new middleware. | ||
|
|
||
| ```ts | ||
| // src/middleware/zen.middleware.ts | ||
| import { shouldBlockRequest } from "@aikidosec/firewall"; | ||
| import { HttpErrors, type Middleware } from "@loopback/rest"; | ||
|
|
||
| export const zenMiddleware: Middleware = async (middlewareCtx, next) => { | ||
| 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: ${result.ip})`; | ||
| } | ||
|
|
||
| throw new HttpErrors.TooManyRequests(message); | ||
| } | ||
|
|
||
| if (result.type === "blocked") { | ||
| throw new HttpErrors.Forbidden("You are blocked by Zen."); | ||
| } | ||
| } | ||
|
|
||
| return next(); | ||
| }; | ||
| ``` | ||
|
|
||
| After creating the middleware, add it to your app: | ||
|
|
||
| ```ts | ||
| // src/applications.ts | ||
|
|
||
| export class Loopback4Application extends BootMixin( | ||
| ServiceMixin(RepositoryMixin(RestApplication)) | ||
| ) { | ||
| constructor(options: ApplicationConfig = {}) { | ||
| super(options); | ||
| this.sequence(MySequence); | ||
| this.bodyParser(JsonBodyParser); | ||
|
|
||
| // ... | ||
|
|
||
| this.middleware(zenMiddleware); // <- Add this line and import the middleware at the top of the file | ||
|
|
||
| // ... | ||
| } | ||
| // ... | ||
| } | ||
| ``` | ||
|
|
||
| ## Debug mode | ||
|
|
||
| If you need to debug the firewall, you can run your NestJS app with the environment variable `AIKIDO_DEBUG` set to `true`: | ||
|
|
||
| ```sh | ||
| AIKIDO_DEBUG=true npm start | ||
| ``` | ||
|
|
||
| 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 [LoopBack 4 example app](../sample-apps/loopback4-psql). |
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,144 @@ | ||
| import { spawn } from "child_process"; | ||
| import { resolve } from "path"; | ||
| import { before, test } from "node:test"; | ||
| import { equal, match, doesNotMatch } from "node:assert"; | ||
| import { getRandomPort } from "./utils/get-port.mjs"; | ||
| import { timeout } from "./utils/timeout.mjs"; | ||
| import { spawnSync } from "node:child_process"; | ||
|
|
||
| const pathToAppDir = resolve( | ||
| import.meta.dirname, | ||
| "../../sample-apps/loopback4-psql" | ||
| ); | ||
|
|
||
| const port = await getRandomPort(); | ||
| const port2 = await getRandomPort(); | ||
|
|
||
| before(async () => { | ||
| spawnSync(`npm`, ["run", "migrate"], { | ||
| cwd: pathToAppDir, | ||
| stdio: "inherit", | ||
| stderr: "inherit", | ||
| }); | ||
| }); | ||
|
|
||
| test("it blocks SQL injection in blocking mode", async () => { | ||
| const server = spawn(`node`, ["dist/index.js"], { | ||
| cwd: pathToAppDir, | ||
| env: { | ||
| ...process.env, | ||
| AIKIDO_DEBUG: "true", | ||
| AIKIDO_BLOCKING: "true", | ||
| PORT: port, | ||
| HOST: "127.0.0.1", | ||
| }, | ||
| }); | ||
|
|
||
| try { | ||
| server.on("error", (err) => { | ||
| throw err; | ||
| }); | ||
|
|
||
| let stdout = ""; | ||
| server.stdout.on("data", (data) => { | ||
| stdout += data.toString(); | ||
| }); | ||
|
|
||
| let stderr = ""; | ||
| server.stderr.on("data", (data) => { | ||
| stderr += data.toString(); | ||
| }); | ||
|
|
||
| await timeout(3000); | ||
|
|
||
| const [sqlInjection, normalRequest, sqlInjectionPath, normalRequestPath] = | ||
| await Promise.all([ | ||
| fetch(`http://127.0.0.1:${port}/insecure-sql`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| username: "admin'); DROP TABLE users;-- -", | ||
| }), | ||
| signal: AbortSignal.timeout(5000), | ||
| }), | ||
| fetch(`http://127.0.0.1:${port}/insecure-sql`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ username: "admin" }), | ||
| signal: AbortSignal.timeout(5000), | ||
| }), | ||
| fetch( | ||
| `http://127.0.0.1:${port}/insecure-sql/${encodeURIComponent("admin' OR '1'='1")}`, | ||
| { signal: AbortSignal.timeout(5000) } | ||
| ), | ||
| fetch(`http://127.0.0.1:${port}/insecure-sql/admin`, { | ||
| signal: AbortSignal.timeout(5000), | ||
| }), | ||
| ]); | ||
|
|
||
| equal(sqlInjection.status, 500); | ||
| equal(normalRequest.status, 200); | ||
| equal(sqlInjectionPath.status, 500); | ||
| equal(normalRequestPath.status, 200); | ||
| match(stdout, /Starting agent/); | ||
|
|
||
| match(stderr, /Zen has blocked an SQL injection/); | ||
| } finally { | ||
| server.kill(); | ||
| } | ||
| }); | ||
|
|
||
| test("it does not block SQL injection in monitoring mode", async () => { | ||
| const server = spawn(`node`, ["dist/index.js"], { | ||
| cwd: pathToAppDir, | ||
| env: { | ||
| ...process.env, | ||
| AIKIDO_DEBUG: "true", | ||
| AIKIDO_BLOCKING: "false", | ||
| PORT: port2, | ||
| HOST: "127.0.0.1", | ||
| }, | ||
| }); | ||
|
|
||
| try { | ||
| server.on("error", (err) => { | ||
| throw err; | ||
| }); | ||
|
|
||
| let stdout = ""; | ||
| server.stdout.on("data", (data) => { | ||
| stdout += data.toString(); | ||
| }); | ||
|
|
||
| let stderr = ""; | ||
| server.stderr.on("data", (data) => { | ||
| stderr += data.toString(); | ||
| }); | ||
|
|
||
| await timeout(3000); | ||
|
|
||
| const [sqlInjection, sqlInjectionPath] = await Promise.all([ | ||
| fetch(`http://127.0.0.1:${port2}/insecure-sql`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| username: "admin'); DROP TABLE users;-- -", | ||
| }), | ||
| signal: AbortSignal.timeout(5000), | ||
| }), | ||
| fetch( | ||
| `http://127.0.0.1:${port2}/insecure-sql/${encodeURIComponent("admin' OR '1'='1")}`, | ||
| { signal: AbortSignal.timeout(5000) } | ||
| ), | ||
| ]); | ||
|
|
||
| match(stdout, /Starting agent/); | ||
| doesNotMatch(await sqlInjection.text(), /Zen has blocked an SQL injection/); | ||
| doesNotMatch( | ||
| await sqlInjectionPath.text(), | ||
| /Zen has blocked an SQL injection/ | ||
| ); | ||
| } 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
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.
Uh oh!
There was an error while loading. Please reload this page.