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
125 changes: 125 additions & 0 deletions benchmarks/idor/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const Zen = require("@aikidosec/firewall");
const express = require("express");
const mysql = require("mysql");

if (process.env.IDOR_ENABLED === "true") {
Zen.enableIdorProtection({
tenantColumnName: "tenant_id",
excludedTables: ["migrations"],
});
}

function getPort() {
const port = parseInt(process.env.PORT, 10) || 4000;

if (isNaN(port)) {
console.error("Invalid port");
process.exit(1);
}

return port;
}

const connection = mysql.createConnection({
host: process.env.MYSQL_HOST || "localhost",
user: process.env.MYSQL_USER || "root",
password: process.env.MYSQL_PASSWORD || "mypassword",
database: process.env.MYSQL_DATABASE || "catsdb",
port: parseInt(process.env.MYSQL_PORT, 10) || 27015,
});

function query(sql, values) {
return new Promise((resolve, reject) => {
connection.query(sql, values, (error, results) => {
if (error) {
return reject(error);
}
resolve(results);
});
});
}

async function setup() {
await query(`
CREATE TABLE IF NOT EXISTS posts (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255),
tenant_id VARCHAR(255)
)
`);
await query(`
CREATE TABLE IF NOT EXISTS comments (
id INT AUTO_INCREMENT PRIMARY KEY,
post_id INT,
body TEXT,
tenant_id VARCHAR(255)
)
`);
await query("DELETE FROM posts");
await query("DELETE FROM comments");

// Seed some data
for (let i = 0; i < 10; i++) {
await query("INSERT INTO posts (title, tenant_id) VALUES (?, ?)", [
`Post ${i}`,
"org_123",
]);
await query(
"INSERT INTO comments (post_id, body, tenant_id) VALUES (?, ?, ?)",
[i + 1, `Comment on post ${i}`, "org_123"]
);
}
}

function start() {
const app = express();

if (process.env.IDOR_ENABLED === "true") {
app.use((req, res, next) => {
Zen.setTenantId("org_123");
next();
});
}

// Cached query: parameterized, always the same SQL string
app.get("/posts", async (req, res) => {
const rows = await query(
"SELECT * FROM posts WHERE tenant_id = ? ORDER BY id LIMIT 10",
["org_123"]
);
res.json(rows);
});

// Cached query: parameterized with a join
app.get("/posts-with-comments", async (req, res) => {
const rows = await query(
`SELECT p.*, c.body AS comment_body
FROM posts p
LEFT JOIN comments c ON p.id = c.post_id AND c.tenant_id = ?
WHERE p.tenant_id = ?
ORDER BY p.id LIMIT 10`,
["org_123", "org_123"]
);
res.json(rows);
});

// Unique query: inline integer ID that changes per request (cache miss)
app.get("/posts/:id", async (req, res) => {
const id = parseInt(req.params.id, 10) || 1;
const rows = await query(
`SELECT * FROM posts WHERE tenant_id = 'org_123' AND id = ${id}`
);
res.json(rows);
});

app.listen(getPort(), () => {
console.log(`Server listening on port ${getPort()}`);
});
}

setup()
.then(() => start())
.catch((err) => {
console.error("Setup failed:", err);
process.exit(1);
});
117 changes: 117 additions & 0 deletions benchmarks/idor/benchmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
const { spawn } = require("child_process");
const { randomInt } = require("crypto");
const { setTimeout } = require("timers/promises");
const http = require("http");

const PORT_WITH_IDOR = 5010;
const PORT_WITHOUT_IDOR = 5011;
const WARMUP_REQUESTS = 1000;
const MEASURED_REQUESTS = 10000;

function percentile(arr, p) {
const i = (arr.length - 1) * p;
const lo = Math.floor(i);
const hi = Math.ceil(i);
return arr[lo] + (arr[hi] - arr[lo]) * (i - lo);
}

function get(port, path) {
return new Promise((resolve, reject) => {
const req = http.get(`http://localhost:${port}${path}`, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => resolve({ status: res.statusCode, data }));
});
req.on("error", reject);
});
}

async function measureRoute(port, path) {
// Warmup
for (let i = 0; i < WARMUP_REQUESTS; i++) {
await get(port, typeof path === "function" ? path() : path);
}

// Measure
const timings = [];
for (let i = 0; i < MEASURED_REQUESTS; i++) {
const url = typeof path === "function" ? path() : path;
const start = performance.now();
await get(port, url);
timings.push(performance.now() - start);
}

timings.sort((a, b) => a - b);

return {
avg: timings.reduce((a, b) => a + b, 0) / timings.length,
p50: percentile(timings, 0.5),
p95: percentile(timings, 0.95),
p99: percentile(timings, 0.99),
};
}

function startServer(port, idorEnabled) {
return spawn("node", ["app.js"], {
cwd: __dirname,
env: {
...process.env,
PORT: String(port),
NODE_OPTIONS: "--require @aikidosec/firewall",
AIKIDO_BLOCK: "true",
IDOR_ENABLED: idorEnabled ? "true" : "false",
},
stdio: "inherit",
});
}

function printComparison(label, withIdor, withoutIdor) {
const diff = withIdor.p50 - withoutIdor.p50;
const pct =
withoutIdor.p50 > 0 ? ((diff / withoutIdor.p50) * 100).toFixed(1) : "N/A";

console.log(`\n${label}`);
console.log(
` Without IDOR avg: ${withoutIdor.avg.toFixed(3)}ms p50: ${withoutIdor.p50.toFixed(3)}ms p95: ${withoutIdor.p95.toFixed(3)}ms p99: ${withoutIdor.p99.toFixed(3)}ms`
);
console.log(
` With IDOR avg: ${withIdor.avg.toFixed(3)}ms p50: ${withIdor.p50.toFixed(3)}ms p95: ${withIdor.p95.toFixed(3)}ms p99: ${withIdor.p99.toFixed(3)}ms`
);
console.log(
` Overhead p50: ${diff >= 0 ? "+" : ""}${diff.toFixed(3)}ms (${pct}%)`
);
}

const routes = [
{ path: "/posts", label: "Cached query (parameterized SELECT)" },
{ path: "/posts-with-comments", label: "Cached query (parameterized JOIN)" },
{
path: () => `/posts/${randomInt(1_000_000)}`,
label: "Unique query (inline ID, cache miss)",
},
];

async function run() {
console.log("Starting servers...");
const procWith = startServer(PORT_WITH_IDOR, true);
const procWithout = startServer(PORT_WITHOUT_IDOR, false);

// Wait for servers + DB setup
await setTimeout(3000);

try {
for (const route of routes) {
const withIdor = await measureRoute(PORT_WITH_IDOR, route.path);
const withoutIdor = await measureRoute(PORT_WITHOUT_IDOR, route.path);
printComparison(route.label, withIdor, withoutIdor);
}
} finally {
procWith.kill();
procWithout.kill();
}
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
Loading