Skip to content
Merged
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
27 changes: 27 additions & 0 deletions .github/workflows/alpine-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: πŸ”οΈ Alpine test

on:
workflow_call:

permissions:
contents: read

jobs:
alpine-test:
runs-on: ubuntu-latest
timeout-minutes: 5
strategy:
fail-fast: false
matrix:
node-version: [16, 18, 20, 22, 24, 25]
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Download build artifacts
uses: actions/download-artifact@v5
with:
name: firewall-node-library-${{ github.sha }}

- name: Test Zen on Alpine (Node.js ${{ matrix.node-version }})
run: bash scripts/test-alpine.sh ${{ matrix.node-version }}
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ jobs:
name: πŸ§ͺ QA Tests
uses: ./.github/workflows/qa-tests.yml
needs: build
alpine-test:
name: πŸ”οΈ Alpine test
uses: ./.github/workflows/alpine-test.yml
needs: build
32 changes: 32 additions & 0 deletions library/helpers/isMusl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Detects if the current process is running on a musl-based system (e.g. Alpine Linux).
* Based on the approach from the detect-libc package.
*/
export function isMusl(): boolean {
if (process.platform !== "linux") {
return false;
}

try {
const report = process.report?.getReport() as Record<string, unknown>;
if (!report) {
return false;
}

const header = report.header as Record<string, unknown> | undefined;
if (header && typeof header.glibcVersionRuntime === "string") {
return false;
}

const sharedObjects = report.sharedObjects as string[] | undefined;
if (Array.isArray(sharedObjects)) {
return sharedObjects.some(
(so) => so.includes("libc.musl-") || so.includes("ld-musl-")
);
}

return false;
} catch {
return false;
}
}
37 changes: 32 additions & 5 deletions library/sinks/FunctionSink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { getLibraryRoot } from "../helpers/getLibraryRoot";
import { getMajorNodeVersion } from "../helpers/getNodeVersion";
import { checkContextForJsInjection } from "../vulnerabilities/js-injection/checkContextForJsInjection";
import { existsSync } from "node:fs";
import { colorText } from "../helpers/colorText";
import { isMusl } from "../helpers/isMusl";

export class FunctionSink implements Wrapper {
private inspectFunction(args: unknown[]): InterceptorResult {
Expand Down Expand Up @@ -40,27 +42,52 @@ export class FunctionSink implements Wrapper {
const platform = process.platform;

const nodeInternalsDir = join(getLibraryRoot(), "node_internals");
const binaryPath = join(
let binaryPath = join(
nodeInternalsDir,
`zen-internals-node-${platform}-${arch}-node${majorVersion}.node`
);
if (isMusl()) {
binaryPath = join(
nodeInternalsDir,
`zen-internals-node-${platform}-${arch}-musl-node${majorVersion}.node`
);
}

if (!existsSync(binaryPath)) {
// oxlint-disable-next-line no-console
console.warn(
`AIKIDO: Cannot find native addon for Node.js ${majorVersion} on ${platform}-${arch}. Code injection attacks via eval() and new Function() will not be blocked. You can request support at https://github.com/AikidoSec/firewall-node/issues`
colorText(
"red",
`AIKIDO: Cannot find native addon for Node.js ${majorVersion} on ${platform}-${arch}. Code injection attacks via eval() and new Function() will not be blocked. You can request support at https://github.com/AikidoSec/firewall-node/issues`
)
);
return;
}

const bindings: {
let bindings: {
setCodeGenerationCallback: (
callback: (code: string) => string | undefined
) => void;
} = require(binaryPath);
};
try {
bindings = require(binaryPath);
} catch (error) {
// oxlint-disable-next-line no-console
console.warn(
colorText(
"red",
`AIKIDO: Failed to load native addon for Node.js ${majorVersion} on ${platform}-${arch}: ${(error as Error).message}. Code injection attacks via eval() and new Function() will not be blocked.`
)
);
return;
}
if (!bindings || typeof bindings.setCodeGenerationCallback !== "function") {
// oxlint-disable-next-line no-console
console.warn(
`AIKIDO: Native addon for Node.js ${majorVersion} on ${platform}-${arch} is invalid. Function sink will not be instrumented.`
colorText(
"red",
`AIKIDO: Native addon for Node.js ${majorVersion} on ${platform}-${arch} is invalid. Function sink will not be instrumented.`
)
);
return;
}
Expand Down
12 changes: 11 additions & 1 deletion scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const INTERNALS_URL = `https://github.com/AikidoSec/zen-internals/releases/downl
// ---

// Node Internals configuration
const NODE_INTERNALS_VERSION = "1.0.0";
const NODE_INTERNALS_VERSION = "1.0.1";
const NODE_INTERNALS_URL = `https://github.com/AikidoSec/zen-internals-node/releases/download/${NODE_INTERNALS_VERSION}`;
// 17 is not included on purpose
const NODE_VERSIONS = [16, 18, 19, 20, 21, 22, 23, 24, 25];
Expand Down Expand Up @@ -141,6 +141,16 @@ async function dlNodeInternals() {
`Downloading Node Internals for Node ${nodeVersion} ${platform} ${arch}...`
);
downloads.push(downloadFile(url, destPath));

// zen-internals-node-linux-x64-musl-node20.node
const muslFilename = `zen-internals-node-${platform}-${arch}-musl-node${nodeVersion}.node`;
const muslUrl = `${NODE_INTERNALS_URL}/${muslFilename}`;
const muslDestPath = join(nodeInternalsDir, muslFilename);

console.log(
`Downloading Node Internals for Node ${nodeVersion} ${platform} ${arch} (musl)...`
);
downloads.push(downloadFile(muslUrl, muslDestPath));
}
}
}
Expand Down
36 changes: 36 additions & 0 deletions scripts/test-alpine.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/bash
set -e

if [ -z "$1" ]; then
echo "Error: Node.js version is required"
echo "Usage: bash scripts/test-alpine.sh <node-version>"
exit 1
fi

NODE_VERSION="$1"

echo "Testing Zen on Alpine Linux (musl) with Node.js ${NODE_VERSION}..."

output=$(docker run --rm -e AIKIDO_DEBUG=true -w /app \
-v "$(pwd)/build:/app/node_modules/@aikidosec/firewall" \
"node:${NODE_VERSION}-alpine" \
node -e "require('@aikidosec/firewall'); setTimeout(() => console.log('OK'), 1000);" 2>&1)

echo "$output"

if echo "$output" | grep -q "Failed to load native addon"; then
echo "FAIL: Native addon failed to load on Alpine"
exit 1
fi

if echo "$output" | grep -q "Cannot find native addon"; then
echo "FAIL: Native addon not found on Alpine"
exit 1
fi

if echo "$output" | grep -q "OK"; then
echo "PASS: Zen loaded successfully on Alpine"
else
echo "FAIL: Zen did not load correctly on Alpine"
exit 1
fi