From 3d0108a04184c3e922e4acb16e025572febcdf26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20K=C3=B6ssler?= Date: Thu, 20 Jun 2024 14:35:11 +0200 Subject: [PATCH] Add xml2js end2end tests --- end2end/tests/hono-xml.test.js | 120 ++++++++++++++++++++++++++++++ package-lock.json | 7 +- sample-apps/hono-xml/Cats.js | 27 +++++++ sample-apps/hono-xml/app.js | 45 +++++++++-- sample-apps/hono-xml/db.js | 25 +++++++ sample-apps/hono-xml/package.json | 1 + 6 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 end2end/tests/hono-xml.test.js create mode 100644 sample-apps/hono-xml/Cats.js create mode 100644 sample-apps/hono-xml/db.js diff --git a/end2end/tests/hono-xml.test.js b/end2end/tests/hono-xml.test.js new file mode 100644 index 000000000..ba0b9c8dd --- /dev/null +++ b/end2end/tests/hono-xml.test.js @@ -0,0 +1,120 @@ +const t = require("tap"); +const { spawn } = require("child_process"); +const { resolve } = require("path"); +const timeout = require("../timeout"); + +const pathToApp = resolve(__dirname, "../../sample-apps/hono-xml", "app.js"); + +t.test("it blocks in blocking mode", (t) => { + const server = spawn(`node`, [pathToApp, "4000"], { + env: { ...process.env, AIKIDO_DEBUG: "true", AIKIDO_BLOCKING: "true" }, + }); + + server.on("close", () => { + t.end(); + }); + + server.on("error", (err) => { + t.fail(err.message); + }); + + let stdout = ""; + server.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + let stderr = ""; + server.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + // Wait for the server to start + timeout(2000) + .then(() => { + return Promise.all([ + fetch("http://localhost:4000/add", { + method: "POST", + body: "Njuska'); DELETE FROM cats;-- H", + headers: { + "Content-Type": "application/xml", + }, + signal: AbortSignal.timeout(5000), + }), + fetch("http://localhost:4000/add", { + method: "POST", + body: "Miau", + headers: { + "Content-Type": "application/xml", + }, + signal: AbortSignal.timeout(5000), + }), + ]); + }) + .then(([sqlInjection, normalAdd]) => { + t.equal(sqlInjection.status, 500); + t.equal(normalAdd.status, 200); + t.match(stdout, /Starting agent/); + t.match(stderr, /Aikido firewall has blocked an SQL injection/); + }) + .catch((error) => { + t.fail(error.message); + }) + .finally(() => { + server.kill(); + }); +}); + +t.test("it does not block in dry mode", (t) => { + const server = spawn(`node`, [pathToApp, "4001"], { + env: { ...process.env, AIKIDO_DEBUG: "true" }, + }); + + server.on("close", () => { + t.end(); + }); + + let stdout = ""; + server.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + let stderr = ""; + server.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + // Wait for the server to start + timeout(2000) + .then(() => + Promise.all([ + fetch("http://localhost:4001/add", { + method: "POST", + body: "Njuska'); DELETE FROM cats;-- H", + headers: { + "Content-Type": "application/xml", + }, + signal: AbortSignal.timeout(5000), + }), + fetch("http://localhost:4001/add", { + method: "POST", + body: "Miau", + headers: { + "Content-Type": "application/xml", + }, + signal: AbortSignal.timeout(5000), + }), + ]) + ) + .then(([sqlInjection, normalAdd]) => { + t.equal(sqlInjection.status, 200); + t.equal(normalAdd.status, 200); + t.match(stdout, /Starting agent/); + t.notMatch(stderr, /Aikido firewall has blocked an SQL injection/); + }) + .catch((error) => { + t.fail(error.message); + }) + .finally(() => { + server.kill(); + }); +}); diff --git a/package-lock.json b/package-lock.json index bcc1f1b82..965d80a3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7412,9 +7412,9 @@ } }, "node_modules/mysql2": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz", - "integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.1.tgz", + "integrity": "sha512-6zo1T3GILsXMCex3YEu7hCz2OXLUarxFsxvFcUHWMpkPtmZLeTTWgRdc1gWyNJiYt6AxITmIf9bZDRy/jAfWew==", "dependencies": { "denque": "^2.1.0", "generate-function": "^2.3.1", @@ -10642,6 +10642,7 @@ "@aikidosec/firewall": "file:../../build", "@hono/node-server": "^1.11.2", "hono": "^4.4.2", + "mysql2": "^3.10.1", "xml2js": "^0.6.2" } }, diff --git a/sample-apps/hono-xml/Cats.js b/sample-apps/hono-xml/Cats.js new file mode 100644 index 000000000..084b48548 --- /dev/null +++ b/sample-apps/hono-xml/Cats.js @@ -0,0 +1,27 @@ +class Cats { + constructor(db) { + this.db = db; + } + + async add(name) { + // This is unsafe! This is for demo purposes only, you should use parameterized queries. + await this.db.query(`INSERT INTO cats(petname) VALUES ('${name}');`); + } + + async byName(name) { + // This is unsafe! This is for demo purposes only, you should use parameterized queries. + const [cats] = await this.db.query( + `SELECT petname FROM cats WHERE petname = '${name}'` + ); + + return cats.map((row) => row.petname); + } + + async getAll() { + const [cats] = await this.db.execute("SELECT petname FROM `cats`;"); + + return cats.map((row) => row.petname); + } +} + +module.exports = Cats; diff --git a/sample-apps/hono-xml/app.js b/sample-apps/hono-xml/app.js index 9002942c2..9df311fe6 100644 --- a/sample-apps/hono-xml/app.js +++ b/sample-apps/hono-xml/app.js @@ -3,10 +3,14 @@ require("@aikidosec/firewall"); const xml2js = require("xml2js"); const { serve } = require("@hono/node-server"); const { Hono } = require("hono"); +const { createConnection } = require("./db"); const Aikido = require("@aikidosec/firewall/context"); +const Cats = require("./Cats"); async function main() { const app = new Hono(); + const db = await createConnection(); + const cats = new Cats(db); app.use(async (c, next) => { Aikido.setUser({ @@ -18,19 +22,37 @@ async function main() { }); app.get("/", async (c) => { + const catNames = await cats.getAll(); return c.html( `

Vulnerable app using XML

+ +
+ + + +
+

SQL Injection: '); DELETE FROM cats;-- H

+ Clear all cats @@ -39,7 +61,7 @@ async function main() { ); }); - app.post("/search", async (c) => { + app.post("/add", async (c) => { const body = await c.req.text(); let result; @@ -50,7 +72,14 @@ async function main() { return c.json({ error: "Invalid XML" }, 400); } - return c.json(result); + await cats.add(result.cat.name[0]); + + return c.json({ success: true }); + }); + + app.get("/clear", async (c) => { + await db.execute("DELETE FROM cats;"); + return c.redirect("/"); }); return app; diff --git a/sample-apps/hono-xml/db.js b/sample-apps/hono-xml/db.js new file mode 100644 index 000000000..101ee7af4 --- /dev/null +++ b/sample-apps/hono-xml/db.js @@ -0,0 +1,25 @@ +const mysql = require("mysql2/promise"); + +async function createConnection() { + // Normally you'd use environment variables for this + const connection = await mysql.createConnection({ + host: "localhost", + user: "root", + password: "mypassword", + database: "catsdb", + port: 27015, + multipleStatements: true, + }); + + await connection.execute(` + CREATE TABLE IF NOT EXISTS cats ( + petname varchar(255) + ); + `); + + return connection; +} + +module.exports = { + createConnection, +}; diff --git a/sample-apps/hono-xml/package.json b/sample-apps/hono-xml/package.json index 756e5d87b..2c9fe301d 100644 --- a/sample-apps/hono-xml/package.json +++ b/sample-apps/hono-xml/package.json @@ -4,6 +4,7 @@ "@aikidosec/firewall": "file:../../build", "@hono/node-server": "^1.11.2", "hono": "^4.4.2", + "mysql2": "^3.10.1", "xml2js": "^0.6.2" } }