Skip to content
Open

1 #233

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
114 changes: 114 additions & 0 deletions n8n/exposed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
Here's the corrected and tightened version:

---

# n8n Exposed REST API

n8n is a workflow automation platform for creating and executing automated workflows. This testbed demonstrates both exposed and properly secured configurations of the n8n REST API.

## Vulnerable Setups

```shell
docker compose -f docker-compose-vuln.yml up -d
```
Early n8n versions (e.g. 0.54.0) shipped without user management. This setup reproduces that behavior: the REST API is fully accessible without authentication and the `/rest/login` endpoint does not exist.

```shell
docker compose -f docker-compose-vuln-no-owner.yml up -d
```
Later 0.x versions introduced user management but allowed instances to run without a configured owner account. In this state, the REST API is accessible without authentication. The `/rest/login` endpoint exists and returns a session cookie on anonymous requests, which can be used in subsequent calls as demonstrated in the steps below.

```shell
docker compose -f docker-compose-vuln-auth.yml up -d
```
Reproduces the BasicAuth split-brain misconfiguration: BasicAuth is enabled and protects the UI (returns `401`), but the `/rest/*` router has no auth middleware applied and remains fully accessible without credentials.

## Safe Setup

```shell
docker compose -f docker-compose-non-vuln.yml up -d
```
A properly configured n8n instance with authentication enforced on all `/rest/*` endpoints. The detector must produce no finding against this target.

## Steps to Reproduce

### Step 1 — Fingerprint

Confirm the target is n8n 0.x by checking for fields that are only present in the 0.x settings schema:

```shell
curl -s http://<target>:5678/rest/settings | jq .
```

Expected response on a vulnerable instance:

```json
{
"data": {
...
"urlBaseWebhook": "http://<target>:5678/",
"versionCli": "0.237.0",
"saveManualExecutions": false,
"saveDataErrorExecution": "all",
...
}
}
```

The presence of `versionCli` and `urlBaseWebhook` confirms this is n8n 0.x. This fingerprint self-scoping — the detector cannot fire against modern instances regardless of their configuration.

### Step 2 — Session Bootstrap (where applicable)

On instances with user management enabled but no owner configured (`docker-compose-vuln-no-owner.yml`), an anonymous `GET /rest/login` returns a valid session cookie:

```shell
curl -s -i http://<target>:5678/rest/login
```

Expected response:

```
HTTP/1.1 200 OK
Set-Cookie: n8n-auth=<jwt>; Path=/; HttpOnly; SameSite=Lax
```

On early versions without user management (`docker-compose-vuln.yml` and `docker-compose-vuln-auth.yml`), this endpoint does not exist. The cookie is not needed — `/rest/workflows` is directly accessible without one.

### Step 3 — Workflows Access

Without cookie (early versions / split-brain):

```shell
curl -s http://<target>:5678/rest/workflows | jq .
```

With cookie (no-owner setup):

```shell
curl -s -H 'Cookie: n8n-auth=<jwt>' http://<target>:5678/rest/workflows | jq .
```

Expected response in both cases:

```json
{
"data": [
{
...
}
]
}
```

Confirms unauthenticated (or pre-authentication anonymous session) access to workflow data.

## Notes

- The vulnerable surface spans all n8n 0.x releases. Early versions (e.g. 0.54.0) had no user management and optional auth that did not cover `/rest/*` routes. Later 0.x versions introduced user management but remained exploitable when no owner account was initialized, or when flags such as `N8N_USER_MANAGEMENT_DISABLED` produced inconsistent auth behavior.
- n8n 1.x enforces authentication at the application layer via session middleware applied directly to the `/rest/` router. Empirical testing confirms that `/rest/workflows` returns `401` on all 1.x+ configurations, including fresh instances with no owner account configured. Reverse proxy misconfigurations that forward unauthenticated requests still reach n8n's own auth check and do not bypass it.
- The `versionCli` and `urlBaseWebhook` fields in `/rest/settings` are unique to n8n 0.x. Their absence in 1.x+ means the fingerprint step will not match modern instances, preventing false positives by design.

## References

- https://github.com/n8n-io/n8n
- https://docs.n8n.io/hosting/securing/overview/
7 changes: 7 additions & 0 deletions n8n/exposed/docker-compose-non-vuln.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
n8n:
image: n8nio/n8n:2.9.1
container_name: n8n-non-vulnerable
ports:
- "5678:5678"
restart: unless-stopped
11 changes: 11 additions & 0 deletions n8n/exposed/docker-compose-vuln-auth.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
n8n:
image: n8nio/n8n:0.54.0
container_name: n8n-vulnerable-auth
ports:
- "5678:5678"
environment:
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=admin
- N8N_BASIC_AUTH_PASSWORD=str0ngpassword!
restart: unless-stopped
7 changes: 7 additions & 0 deletions n8n/exposed/docker-compose-vuln-no-owner.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
n8n:
image: n8nio/n8n:0.237.0
container_name: n8n-vulnerable-no-owner
ports:
- "5678:5678"
restart: unless-stopped
7 changes: 7 additions & 0 deletions n8n/exposed/docker-compose-vuln.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
n8n:
image: n8nio/n8n:0.54.0
container_name: n8n-vulnerable
ports:
- "5678:5678"
restart: unless-stopped
3 changes: 3 additions & 0 deletions osduplicate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# OS duplicate testbeds

This folder contains testbeds for validating scalibr `osduplicate` Annotators
26 changes: 26 additions & 0 deletions osduplicate/apk/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM alpine:3.21

# Install a pkg from main repos
RUN apk add fzf

# Verify that the extra pkg is not available
RUN ! apk add --simulate orb

# Add external repository
RUN wget -O /etc/apk/keys/packages@orb.net.rsa.pub https://pkgs.orb.net/stable/alpine/orb.pub
RUN echo "https://pkgs.orb.net/stable/alpine" >> /etc/apk/repositories
RUN apk update

# Install extra pkg
RUN apk add orb

COPY verify.sh /usr/bin/verify.sh
RUN chmod +x /usr/bin/verify.sh

ENV DEFAULT_PKG="fzf"
ENV DEFAULT_PKG_FILE="usr/bin/fzf"
ENV EXTRA_PKG="orb"
ENV EXTRA_PKG_FILE="usr/bin/orb"
ENV LANGUAGE_EXTRACTORS="go/binary"
ENV OS_EXTRACTOR="os/apk"
ENV DUP_ANNOTATOR="vex/os-duplicate/apk"
26 changes: 26 additions & 0 deletions osduplicate/apk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# APK OS Testbed

This testbed is developed to verify that scalibr adds the exploitability signals (related to `osduplicate`) only to packages which were added through default repositories.

Inventories returned from files which are part of `apk` packages installed via default repository receive an OS-level advisory which is more precise and less prone to false positives, while packages installed by other means do not receive any OS-level advisory and should be covered using language-level extractors.

In this example the `Dockerfile` installs:

- `fzf` from a default repository
- `orb` from a custom repository

## Verification Steps

Build the image and run the testbed:

```sh
docker build --platform linux/amd64 -t osduplicate-apk -f Dockerfile ..
docker run --rm --platform linux/amd64 -v /tmp/scalibr:/usr/bin/scalibr osduplicate-apk /usr/bin/verify.sh
```

For manual verification launch:

```sh
docker build --platform linux/amd64 -t osduplicate-apk . -f Dockerfile ..
docker run --rm -it --platform linux/amd64 -v /tmp/scalibr:/usr/bin/scalibr osduplicate-apk
```
29 changes: 29 additions & 0 deletions osduplicate/dpkg/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FROM debian:bookworm

RUN apt update && apt-get install -y software-properties-common curl

# Verify that spark-core is not available in default repos
RUN ! apt install spark-core

# Install fzf from the default repos
RUN apt install fzf

# Add extra repository
RUN sh -c "echo 'deb [signed-by=/etc/apt/keyrings/dataproc.gpg] https://storage.googleapis.com/dataproc-bigtop-repo/2_3_deb12_20251203_064720-RC01 dataproc contrib' > /etc/apt/sources.list.d/dataproc.list"
RUN sh -c "curl -s https://storage.googleapis.com/dataproc-bigtop-repo/2_3_deb12_20251203_064720-RC01/archive.key | gpg --dearmor -o /etc/apt/keyrings/dataproc.gpg"
RUN apt-get update

# Add extra pkg
RUN apt-get install -y spark-core


COPY verify.sh /usr/bin/verify.sh
RUN chmod +x /usr/bin/verify.sh

ENV DEFAULT_PKG="fzf"
ENV DEFAULT_PKG_FILE="usr/bin/fzf"
ENV EXTRA_PKG="spark-core"
ENV EXTRA_PKG_FILE="usr/lib/spark/jars/spark-core_2.12-3.5.3.jar"
ENV LANGUAGE_EXTRACTORS="go/binary,java/archive"
ENV OS_EXTRACTOR="os/dpkg"
ENV DUP_ANNOTATOR="vex/os-duplicate/dpkg"
26 changes: 26 additions & 0 deletions osduplicate/dpkg/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# DPKG OS Testbed

This testbed is developed to verify that scalibr adds the exploitability signals (related to `osduplicate`) only to packages which were added through default repositories.

Inventories returned from files which are part of `dpkg` packages installed via default repository receive an OS-level advisory which is more precise and less prone to false positives, while packages installed by other means do not receive any OS-level advisory and should be covered using language-level extractors.

In this example the `Dockerfile` installs:

- `fzf` from a default repository
- `spark-core` from a custom repository

## Verification Steps

Build the image and run the testbed:

```sh
docker build --platform linux/amd64 -t osduplicate-dpkg -f Dockerfile ..
docker run --rm -it --platform linux/amd64 -v /tmp/scalibr:/usr/bin/scalibr osduplicate-dpkg /usr/bin/verify.sh
```

For manual verification launch:

```sh
docker build --platform linux/amd64 -t osduplicate-dpkg -f Dockerfile ..
docker run --rm -it --platform linux/amd64 -v /tmp/scalibr:/usr/bin/scalibr osduplicate-dpkg
```
21 changes: 21 additions & 0 deletions osduplicate/rpm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# RPM OS Testbed

This testbed is developed to verify that scalibr adds the exploitability signals (related to `osduplicate`) only to packages which were added through default repositories.

Inventories returned from files which are part of `rpm` packages installed via default repository receive an OS-level advisory which is more precise and less prone to false positives, while packages installed by other means do not receive any OS-level advisory and should be covered using language-level extractors.

The docker-compose contains multiple docker images which install a pkg from a default repository and from one from an extra one. Then the `verify.sh` script is used to ensure that scalibr correctly adds `exploitability_signals` only to the pkg installed via the default repo.

## Verification Steps

Build the image and run the testbed:

```sh
docker compose up --build
```

For manual verification launch:

```sh
docker compose run amazonlinux2 bash
```
7 changes: 7 additions & 0 deletions osduplicate/rpm/common.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DEFAULT_PKG=runc
DEFAULT_PKG_FILE=usr/bin/runc
EXTRA_PKG=gh
EXTRA_PKG_FILE=usr/bin/gh
LANGUAGE_EXTRACTORS=go/binary
OS_EXTRACTOR=os/rpm
DUP_ANNOTATOR=vex/os-duplicate/rpm
Loading
Loading