Skip to content

Commit

Permalink
Add sample app and e2e test
Browse files Browse the repository at this point in the history
  • Loading branch information
timokoessler committed Feb 11, 2025
1 parent 192dfb5 commit 8201144
Show file tree
Hide file tree
Showing 6 changed files with 942 additions and 0 deletions.
109 changes: 109 additions & 0 deletions end2end/tests/hapi-libsql.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const t = require("tap");
const { spawn } = require("child_process");
const { resolve } = require("path");
const timeout = require("../timeout");

const pathToApp = resolve(__dirname, "../../sample-apps/hapi-libsql", "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://127.0.0.1:4000/?petname=${encodeURIComponent("Njuska'); DELETE FROM cats;-- H")}`,
{
signal: AbortSignal.timeout(5000),
}
),
fetch("http://127.0.0.1:4000/?petname=Njuska", {
signal: AbortSignal.timeout(5000),
}),
]);
})
.then(async ([noSQLInjection, normalSearch]) => {
t.equal(noSQLInjection.status, 500);
t.equal(normalSearch.status, 200);
t.match(stdout, /Starting agent/);
t.match(await noSQLInjection.text(), /Zen 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://127.0.0.1:4001/?petname=${encodeURIComponent("Njuska'); DELETE FROM cats;-- H")}`,
{
signal: AbortSignal.timeout(5000),
}
),
fetch("http://127.0.0.1:4001/?petname=Njuska", {
signal: AbortSignal.timeout(5000),
}),
])
)
.then(async ([noSQLInjection, normalSearch]) => {
t.equal(noSQLInjection.status, 200);
t.equal(normalSearch.status, 200);
t.match(stdout, /Starting agent/);
t.notMatch(
await noSQLInjection.text(),
/Zen has blocked an SQL injection/
);
})
.catch((error) => {
t.fail(error.message);
})
.finally(() => {
server.kill();
});
});
29 changes: 29 additions & 0 deletions sample-apps/hapi-libsql/Cats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class Cats {
/**
*
* @param {import("@libsql/client").Client} db
*/
constructor(db) {
this.db = db;
}

async add(name) {
return await this.db.executeMultiple(
`INSERT INTO cats(petname) VALUES ('${name}');`
);
}

async byName(name) {
const cats = await this.db.executeMultiple(
`SELECT petname FROM cats WHERE petname = '${name}';`
);
return cats.rows.map((row) => row.petname);
}

async getAll() {
const cats = await this.db.execute("SELECT petname FROM cats;");
return cats.rows.map((row) => row.petname);
}
}

module.exports = Cats;
11 changes: 11 additions & 0 deletions sample-apps/hapi-libsql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# hapi-libsql

WARNING: This application contains security issues and should not be used in production (or taken as an example of how to write secure code).

In the root directory run `npm run sample-app hapi-libsql` to start the server.

Try the following URLs:

- http://localhost:4000/ : List all cats
- http://localhost:4000/?petname=Kitty : This will add a new cat named "Kitty"
- http://localhost:4000/?petname=Kitty'); DELETE FROM cats;-- H : This will delete all cats
92 changes: 92 additions & 0 deletions sample-apps/hapi-libsql/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const Zen = require("@aikidosec/firewall");

const Cats = require("./Cats");
const Hapi = require("@hapi/hapi");
const { createClient } = require("@libsql/client");

require("@aikidosec/firewall/nopp");

function getHTMLBody(cats) {
return `
<html lang="en">
<body>
<p>All cats : ${cats.join(", ")}</p>
<form action="/" method="GET">
<label for="search">Add a new cat</label>
<input type="text" name="petname">
<input type="submit" value="Add" />
</form>
<a href="http://localhost:4000/?petname=Kitty'); DELETE FROM cats;-- H">Test injection</a> / <a href="http://localhost:4000/clear">Clear table</a>
</body>
</html>`;
}

async function createConnection() {
/** @type {import("@libsql/client".Client)} */
const client = createClient({
url: ":memory:",
});

await client.execute(`
CREATE TABLE IF NOT EXISTS cats (
petname varchar(255)
);
`);

return client;
}

async function init(port) {
const db = await createConnection();
const cats = new Cats(db);

const server = new Hapi.Server({
port: port,
host: "127.0.0.1",
});

Zen.addHapiMiddleware(server);

server.route({
method: "GET",
path: "/",
handler: async (request, h) => {
try {
if (request.query.petname) {
await cats.add(request.query.petname);
}
} catch (e) {
return h.response(e.message).code(500);
}

return getHTMLBody(await cats.getAll());
},
});

server.route([
{
method: "GET",
path: "/clear",
handler: async (request, h) => {
await db.query("DELETE FROM cats;");
return h.redirect("/");
},
},
]);

await server.start();
console.log(`Server running on http://127.0.0.1:${port}`);
}

function getPort() {
const port = parseInt(process.argv[2], 10) || 4000;

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

return port;
}

init(getPort());
Loading

0 comments on commit 8201144

Please sign in to comment.