diff --git a/app/data/reports-dao.js b/app/data/reports-dao.js new file mode 100644 index 0000000000..cd85b38535 --- /dev/null +++ b/app/data/reports-dao.js @@ -0,0 +1,138 @@ +/* + * A1 - SQL Injection + * + * This module demonstrates SQL Injection vulnerabilities in a SQLite-backed + * payroll reports feature. User-supplied input is concatenated directly into + * SQL query strings instead of using parameterized queries. + * + * Attack examples: + * Search name: ' OR '1'='1 -> dumps all employee records + * Search name: ' OR 1=1-- -> bypasses filtering + * Search name: '; DROP TABLE employees;-- -> destructive injection + * Search name: ' UNION SELECT id,username,password,salary,0 FROM users-- -> data exfil + */ + +const sqlite3 = require("sqlite3").verbose(); +const path = require("path"); + +// Use an in-memory database pre-seeded with sample payroll data +let dbInstance = null; + +function getDb() { + if (dbInstance) return dbInstance; + + dbInstance = new sqlite3.Database(":memory:"); + + dbInstance.serialize(() => { + // Create employees table with sensitive payroll data + dbInstance.run(` + CREATE TABLE IF NOT EXISTS employees ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + department TEXT NOT NULL, + salary INTEGER NOT NULL, + ssn TEXT NOT NULL + ) + `); + + // Create a shadow users table (exfiltrable via UNION injection) + dbInstance.run(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + password TEXT NOT NULL, + salary INTEGER, + is_admin INTEGER DEFAULT 0 + ) + `); + + // Seed employees + const employees = [ + ["Alice Johnson", "Engineering", 95000, "123-45-6789"], + ["Bob Smith", "Marketing", 72000, "987-65-4321"], + ["Carol White", "HR", 68000, "456-78-9012"], + ["David Brown", "Finance", 88000, "321-54-9876"], + ["Eve Davis", "Engineering", 102000, "654-32-1098"] + ]; + const insertEmp = dbInstance.prepare( + "INSERT INTO employees (name, department, salary, ssn) VALUES (?, ?, ?, ?)" + ); + employees.forEach(e => insertEmp.run(e)); + insertEmp.finalize(); + + // Seed users (simulates credential store accessible via UNION injection) + const users = [ + [1, "admin", "s3cr3tAdmin!", 0, 1], + [2, "user1", "Password123", 95000, 0], + [3, "user2", "qwerty", 72000, 0] + ]; + const insertUser = dbInstance.prepare( + "INSERT INTO users (id, username, password, salary, is_admin) VALUES (?, ?, ?, ?, ?)" + ); + users.forEach(u => insertUser.run(u)); + insertUser.finalize(); + }); + + return dbInstance; +} + +/* ReportsDAO provides payroll search functionality */ +function ReportsDAO() { + "use strict"; + + if (false === (this instanceof ReportsDAO)) { + console.log("Warning: ReportsDAO constructor called without 'new' operator"); + return new ReportsDAO(); + } + + const db = getDb(); + + /* + * VULNERABLE: searchEmployees builds a query via string concatenation. + * The `name` parameter comes directly from req.query.name with no + * sanitization or parameterization, allowing classic SQL injection. + * + * Fix (A1): Use a parameterized query instead: + * const query = "SELECT id, name, department, salary FROM employees WHERE name LIKE ?"; + * db.all(query, [`%${name}%`], callback); + */ + this.searchEmployees = (name, callback) => { + // Insecure: user input concatenated directly into SQL string + const query = `SELECT id, name, department, salary FROM employees WHERE name LIKE '%${name}%'`; + + console.log(`[ReportsDAO] Executing query: ${query}`); + + db.all(query, (err, rows) => { + if (err) { + return callback(err, null); + } + return callback(null, rows); + }); + }; + + /* + * VULNERABLE: getEmployeeById fetches a single employee by ID using + * string interpolation. An attacker can inject UNION SELECT to exfiltrate + * data from other tables. + * + * Payload: 0 UNION SELECT id, username, password, is_admin FROM users-- + * + * Fix (A1): Use parameterized query: + * db.get("SELECT * FROM employees WHERE id = ?", [id], callback); + */ + this.getEmployeeById = (id, callback) => { + // Insecure: id from request URL parameter concatenated into query + const query = `SELECT * FROM employees WHERE id = ${id}`; + + console.log(`[ReportsDAO] Executing query: ${query}`); + + db.get(query, (err, row) => { + if (err) { + return callback(err, null); + } + return callback(null, row); + }); + }; +} + +module.exports = { ReportsDAO }; diff --git a/app/routes/index.js b/app/routes/index.js index a9e55426bf..ced0fdc454 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -5,6 +5,7 @@ const ContributionsHandler = require("./contributions"); const AllocationsHandler = require("./allocations"); const MemosHandler = require("./memos"); const ResearchHandler = require("./research"); +const ReportsHandler = require("./reports"); const tutorialRouter = require("./tutorial"); const ErrorHandler = require("./error").errorHandler; @@ -19,6 +20,7 @@ const index = (app, db) => { const allocationsHandler = new AllocationsHandler(db); const memosHandler = new MemosHandler(db); const researchHandler = new ResearchHandler(db); + const reportsHandler = new ReportsHandler(); // Middleware to check if a user is logged in const isLoggedIn = sessionHandler.isLoggedInMiddleware; @@ -75,6 +77,10 @@ const index = (app, db) => { // Research Page app.get("/research", isLoggedIn, researchHandler.displayResearch); + // Reports Page - A1: SQL Injection via SQLite string concatenation + app.get("/reports", isLoggedIn, reportsHandler.searchEmployees); + app.get("/reports/employee/:id", isLoggedIn, reportsHandler.getEmployee); + // Mount tutorial router app.use("/tutorial", tutorialRouter); diff --git a/app/routes/reports.js b/app/routes/reports.js new file mode 100644 index 0000000000..7148b1c15f --- /dev/null +++ b/app/routes/reports.js @@ -0,0 +1,90 @@ +const { ReportsDAO } = require("../data/reports-dao"); +const { environmentalScripts } = require("../../config/config"); + +function ReportsHandler() { + "use strict"; + + const reportsDAO = new ReportsDAO(); + + this.displayReports = (req, res, next) => { + const { userId } = req.session; + + return res.render("payroll", { + userId, + employees: null, + searchName: "", + environmentalScripts + }); + }; + + /* + * A1 - SQL Injection + * The search term from req.query.name is passed directly to ReportsDAO.searchEmployees + * which concatenates it into a raw SQL string. + * + * Attack: search for ' OR '1'='1 to dump all records. + * Attack: search for ' UNION SELECT id,username,password,salary,0 FROM users-- + * to exfiltrate the users table via a UNION-based injection. + * + * Fix: sanitize/validate input before passing to DAO, or use parameterized + * queries in the DAO layer (see comments in reports-dao.js). + */ + this.searchEmployees = (req, res, next) => { + const { userId } = req.session; + // Insecure: raw query parameter forwarded to DAO without sanitization + const searchName = req.query.name || ""; + + reportsDAO.searchEmployees(searchName, (err, employees) => { + if (err) { + // Surface the raw DB error so attackers can observe schema info (A6) + return res.render("payroll", { + userId, + employees: [], + searchName, + dbError: err.message, + environmentalScripts + }); + } + + return res.render("payroll", { + userId, + employees, + searchName, + environmentalScripts + }); + }); + }; + + /* + * A1 - SQL Injection (second-order / numeric injection) + * The :id URL parameter is interpolated directly into a SQL query in the DAO. + * + * Attack: GET /reports/employee/0 UNION SELECT id,username,password,salary,0 FROM users-- + */ + this.getEmployee = (req, res, next) => { + const { userId } = req.session; + // Insecure: raw URL parameter passed to DAO without parseInt or validation + const empId = req.params.id; + + reportsDAO.getEmployeeById(empId, (err, employee) => { + if (err) { + return res.render("payroll", { + userId, + employees: [], + searchName: "", + dbError: err.message, + environmentalScripts + }); + } + + return res.render("payroll", { + userId, + employees: employee ? [employee] : [], + searchName: "", + environmentalScripts + }); + }); + }; +} + +module.exports = ReportsHandler; diff --git a/app/views/layout.html b/app/views/layout.html index 380ba414b0..c146e7715b 100644 --- a/app/views/layout.html +++ b/app/views/layout.html @@ -63,6 +63,8 @@
  • Research
  • +
  • Reports +
  • {% endif %}
  • Logout
  • diff --git a/app/views/payroll.html b/app/views/payroll.html new file mode 100644 index 0000000000..307a819c22 --- /dev/null +++ b/app/views/payroll.html @@ -0,0 +1,82 @@ +{% extends "./layout.html" %} {% block title %}Payroll Reports{% endblock %} {% block content %} + +
    +
    + +
    +
    +

    Employee Payroll Search

    +
    +
    + + +
    +
    + + + + +
    +

    + Hint (A1 - SQL Injection): + Try searching for ' OR '1'='1 to dump all records, or + ' UNION SELECT id,username,password,salary,0 FROM users-- + to exfiltrate the users table. +

    +
    + +
    +
    + + {% if dbError %} +
    + Database Error: {{ dbError }} +
    + {% endif %} + + {% if employees %} +
    +
    +

    Results

    +
    +
    + + + + + + + + + + + + {% for emp in employees %} + + + + + + + + {% endfor %} + +
    IDNameDepartmentSalaryActions
    {{ emp.id }}{{ emp.name }}{{ emp.department }}${{ emp.salary }} + + + View Detail + +
    +
    +
    + {% endif %} + +
    +
    +{% endblock %} diff --git a/package.json b/package.json index b2eb65a041..973a56e03d 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,27 @@ "helmet": "^2.0.0", "marked": "0.3.5", "mongodb": "^2.1.18", + "sqlite3": "^5.1.6", "needle": "2.2.4", "node-esapi": "0.0.1", "serve-favicon": "^2.3.0", "swig": "^1.4.2", - "underscore": "^1.8.3" + "underscore": "^1.8.3", + "lodash": "4.17.4", + "minimist": "0.0.8", + "handlebars": "4.0.11", + "serialize-javascript": "2.1.1", + "node-uuid": "1.4.7" }, "comments": { - "//": "a9 insecure components" + "//": "a9 insecure components", + "vulnerable-packages": { + "lodash@4.17.4": "CVE-2019-10744 - prototype pollution via _.defaultsDeep / _.merge / _.mergeWith", + "minimist@0.0.8": "CVE-2020-7598 - prototype pollution via --__proto__ CLI flag parsing", + "handlebars@4.0.11": "CVE-2019-20920 / CVE-2019-19919 - prototype pollution + RCE via template compilation", + "serialize-javascript@2.1.1": "CVE-2019-16769 - XSS via unescaped in serialized regexes", + "node-uuid@1.4.7": "insecure random number generation; replaced by 'uuid' package" + } }, "scripts": { "start": "node server.js",