Skip to content

Commit

Permalink
Merge branch 'main' of github.com:AikidoSec/node-RASP into patch-ssrf…
Browse files Browse the repository at this point in the history
…-enable

* 'main' of github.com:AikidoSec/node-RASP: (113 commits)
  Add performance test
  Shorten function
  Shorten methods
  Fix tests for attackPath
  Add failing tests
  Add breaking test
  Add test
  Add more tests
  Improve test coverage
  Extend and fix tests
  Link to Aikido Blog for Command Injection attacks
  Use more efficient ip matcher
  Fix tests
  Fix again
  fix: Wrong payload path after merge
  Fix tests
  Delete jwt.iss
  Add test
  Use new Zen internals JS parser
  Update Zen Internals
  ...
  • Loading branch information
hansott committed Dec 18, 2024
2 parents d197d60 + a892b72 commit 51c055a
Show file tree
Hide file tree
Showing 135 changed files with 34,514 additions and 5,748 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
INTERNALS_VERSION = v0.1.31
INTERNALS_VERSION = v0.1.32
INTERNALS_URL = https://github.com/AikidoSec/zen-internals/releases/download/$(INTERNALS_VERSION)
TARBALL = zen_internals.tgz
CHECKSUM_FILE = zen_internals.tgz.sha256sum
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ Zen will autonomously protect your Node.js applications against:

* 🛡️ [NoSQL injection attacks](https://www.aikido.dev/blog/web-application-security-vulnerabilities)
* 🛡️ [SQL injection attacks](https://www.aikido.dev/blog/the-state-of-sql-injections)
* 🛡️ [Command injection attacks](https://owasp.org/www-community/attacks/Command_Injection)
* 🛡️ [Command injection attacks](https://www.aikido.dev/blog/command-injection-in-2024-unpacked)
* 🛡️ [Prototype pollution](./docs/prototype-pollution.md)
* 🛡️ [Path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal)
* 🛡️ [Server-side request forgery (SSRF)](./docs/ssrf.md)
* 🛡️ JS injection

Zen operates autonomously on the same server as your Node.js app to:

Expand Down Expand Up @@ -111,7 +112,14 @@ $ yarn add --exact @aikidosec/firewall

For framework- and provider- specific instructions, check out our docs:

- [Express.js-based apps](docs/express.md)
- [Express](docs/express.md)
- [Fastify](docs/fastify.md)
- [Hapi](docs/hapi.md)
- [Koa](docs/koa.md)
- [Hono](docs/hono.md)
- [NestJS](docs/nestjs.md)
- [micro](docs/micro.md)
- [Next.js](docs/next.md)
- [AWS Lambda](docs/lambda.md)
- [Google Cloud Functions](docs/cloud-functions.md)
- [Google Cloud Pub/Sub](docs/pubsub.md)
Expand Down
35 changes: 35 additions & 0 deletions docs/esbuild.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Installing Zen in a Node.js Application Bundled with esbuild

Note: Zen runs only on the server side, it does not run in the browser.

Note: If `bundle` is set to `false` in the esbuild configuration, Zen will work without any additional configuration.

Modify your esbuild configuration to include the external option using this utility:

```js
const { build } = require("esbuild");
const { externals } = require("@aikidosec/firewall/bundler"); // <-- Add this line

build({
entryPoints: ["./app.js"],
bundle: true,
platform: "node",
target: "node18",
outfile: "./dist/app.js",
external: externals(), // <-- Add this line
});
```

This tells esbuild to exclude @aikidosec/firewall and any packages that Zen hooks into from the bundle.

⚠️ Don't forget to copy the node_modules directory to the output directory.

## Why do I need to do this?

Zen works by intercepting `require()` calls that a Node.js application makes when loading modules. This includes modules that are built-in to Node.js, like the `fs` module for accessing the filesystem, as well as modules installed from the NPM registry, like the `pg` database module.

Bundlers like esbuild crawl all of the `require()` calls that an application makes to files on disk. It replaces the `require()` calls with custom code and combines all the resulting JavaScript into one "bundled" file. When a built-in module is loaded, such as `require('fs')`, that call can then remain the same in the resulting bundle.

Zen can continue to intercept the calls for built-in modules but cannot intercept calls to third party libraries under those conditions. This means that when you bundle a Zen app with a bundler Zen is likely to capture information about disk access (through `fs`) and outbound HTTP requests (through `http`), but omit calls to third party libraries.

The solution is to treat all third party modules that Zen needs to instrument as being "external" to the bundler. With this setting the instrumented modules remain on disk and continue to be loaded with `require()` while the non-instrumented modules are bundled.
7 changes: 6 additions & 1 deletion end2end/server/app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// This is a insecure mock server for testing purposes
// This is an insecure mock server for testing purposes
const express = require("express");
const config = require("./src/handlers/getConfig");
const captureEvent = require("./src/handlers/captureEvent");
const listEvents = require("./src/handlers/listEvents");
const createApp = require("./src/handlers/createApp");
const checkToken = require("./src/middleware/checkToken");
const updateConfig = require("./src/handlers/updateConfig");
const ipLists = require("./src/handlers/ipLists");
const updateIPLists = require("./src/handlers/updateIPLists");

const app = express();

Expand All @@ -19,6 +21,9 @@ app.post("/api/runtime/config", checkToken, updateConfig);
app.get("/api/runtime/events", checkToken, listEvents);
app.post("/api/runtime/events", checkToken, captureEvent);

app.get("/api/runtime/firewall/lists", checkToken, ipLists);
app.post("/api/runtime/firewall/lists", checkToken, updateIPLists);

app.post("/api/runtime/apps", createApp);

app.listen(port, () => {
Expand Down
24 changes: 24 additions & 0 deletions end2end/server/src/handlers/ipLists.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const { getBlockedIPAddresses } = require("../zen/config");

module.exports = function ipLists(req, res) {
if (!req.app) {
throw new Error("App is missing");
}

const blockedIps = getBlockedIPAddresses(req.app);

res.json({
success: true,
serviceId: req.app.id,
blockedIPAddresses:
blockedIps.length > 0
? [
{
source: "geoip",
description: "geo restrictions",
ips: blockedIps,
},
]
: [],
});
};
32 changes: 32 additions & 0 deletions end2end/server/src/handlers/updateIPLists.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { updateBlockedIPAddresses } = require("../zen/config");

module.exports = function updateIPLists(req, res) {
if (!req.app) {
throw new Error("App is missing");
}

// Insecure input validation - but this is only a mock server
if (
!req.body ||
typeof req.body !== "object" ||
Array.isArray(req.body) ||
!Object.keys(req.body).length
) {
return res.status(400).json({
message: "Request body is missing or invalid",
});
}

if (
!req.body.blockedIPAddresses ||
!Array.isArray(req.body.blockedIPAddresses)
) {
return res.status(400).json({
message: "blockedIPAddresses is missing or invalid",
});
}

updateBlockedIPAddresses(req.app, req.body.blockedIPAddresses);

res.json({ success: true });
};
34 changes: 33 additions & 1 deletion end2end/server/src/zen/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,43 @@ function updateAppConfig(app, newConfig) {
getAppConfig(app);
index = configs.length - 1;
}
configs[index] = { ...configs[index], ...newConfig };
configs[index] = {
...configs[index],
...newConfig,
lastUpdatedAt: Date.now(),
};
return true;
}

const blockedIPAddresses = [];

function updateBlockedIPAddresses(app, ips) {
let entry = blockedIPAddresses.find((ip) => ip.serviceId === app.serviceId);

if (entry) {
entry.ipAddresses = ips;
} else {
entry = { serviceId: app.serviceId, ipAddresses: ips };
blockedIPAddresses.push(entry);
}

// Bump lastUpdatedAt
updateAppConfig(app, {});
}

function getBlockedIPAddresses(app) {
const entry = blockedIPAddresses.find((ip) => ip.serviceId === app.serviceId);

if (entry) {
return entry.ipAddresses;
}

return { serviceId: app.serviceId, ipAddresses: [] };
}

module.exports = {
getAppConfig,
updateAppConfig,
updateBlockedIPAddresses,
getBlockedIPAddresses,
};
12 changes: 10 additions & 2 deletions end2end/tests/express-mongodb.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@ t.test("it blocks in blocking mode", (t) => {
fetch("http://127.0.0.1:4000/?search[$ne]=null", {
signal: AbortSignal.timeout(5000),
}),
fetch("http://127.0.0.1:4000/where?title=Test%27%7C%7C%27a", {
signal: AbortSignal.timeout(5000),
}),
fetch("http://127.0.0.1:4000/?search=title", {
signal: AbortSignal.timeout(5000),
}),
]);
})
.then(([noSQLInjection, normalSearch]) => {
.then(([noSQLInjection, jsInjection, normalSearch]) => {
t.equal(noSQLInjection.status, 500);
t.equal(jsInjection.status, 500);
t.equal(normalSearch.status, 200);
t.match(stdout, /Starting agent/);
t.match(stderr, /Zen has blocked a NoSQL injection/);
Expand Down Expand Up @@ -86,13 +90,17 @@ t.test("it does not block in dry mode", (t) => {
fetch("http://127.0.0.1:4001/?search[$ne]=null", {
signal: AbortSignal.timeout(5000),
}),
fetch("http://127.0.0.1:4001/where?title=Test%27%7C%7C%27a", {
signal: AbortSignal.timeout(5000),
}),
fetch("http://127.0.0.1:4001/?search=title", {
signal: AbortSignal.timeout(5000),
}),
])
)
.then(([noSQLInjection, normalSearch]) => {
.then(([noSQLInjection, jsInjection, normalSearch]) => {
t.equal(noSQLInjection.status, 200);
t.equal(jsInjection.status, 200);
t.equal(normalSearch.status, 200);
t.match(stdout, /Starting agent/);
t.notMatch(stderr, /Zen has blocked a NoSQL injection/);
Expand Down
Loading

0 comments on commit 51c055a

Please sign in to comment.