{
  "name": "security-sql-injection",
  "description": "Find and fix SQL injection vulnerabilities in a user management module",
  "dataset": "terminal-bench-local",
  "difficulty": "medium",
  "instruction": "The file user_db.py contains a UserDB class that manages users in a SQLite database. It has SQL injection vulnerabilities — user input is concatenated directly into SQL queries. Fix ALL SQL injection vulnerabilities by using parameterized queries. Do NOT change test_user_db.py. The tests verify both correct functionality AND that injection attacks are properly prevented.",
  "setup_files": {
    "user_db.py": "import sqlite3\n\nclass UserDB:\n    def __init__(self, db_path=':memory:'):\n        self.conn = sqlite3.connect(db_path)\n        self.conn.execute('''CREATE TABLE IF NOT EXISTS users (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            username TEXT UNIQUE NOT NULL,\n            email TEXT NOT NULL,\n            role TEXT DEFAULT 'user'\n        )''')\n        self.conn.commit()\n\n    def add_user(self, username: str, email: str, role: str = 'user') -> int:\n        \"\"\"Add a user and return their id.\"\"\"\n        cursor = self.conn.execute(\n            f\"INSERT INTO users (username, email, role) VALUES ('{username}', '{email}', '{role}')\"\n        )\n        self.conn.commit()\n        return cursor.lastrowid\n\n    def get_user(self, username: str) -> dict | None:\n        \"\"\"Get a user by username.\"\"\"\n        cursor = self.conn.execute(\n            f\"SELECT id, username, email, role FROM users WHERE username = '{username}'\"\n        )\n        row = cursor.fetchone()\n        if row:\n            return {'id': row[0], 'username': row[1], 'email': row[2], 'role': row[3]}\n        return None\n\n    def search_users(self, query: str) -> list[dict]:\n        \"\"\"Search users by username pattern (LIKE match).\"\"\"\n        cursor = self.conn.execute(\n            f\"SELECT id, username, email, role FROM users WHERE username LIKE '%{query}%'\"\n        )\n        return [{'id': r[0], 'username': r[1], 'email': r[2], 'role': r[3]} for r in cursor.fetchall()]\n\n    def update_role(self, username: str, new_role: str):\n        \"\"\"Update a user's role.\"\"\"\n        self.conn.execute(\n            f\"UPDATE users SET role = '{new_role}' WHERE username = '{username}'\"\n        )\n        self.conn.commit()\n\n    def delete_user(self, username: str):\n        \"\"\"Delete a user by username.\"\"\"\n        self.conn.execute(\n            f\"DELETE FROM users WHERE username = '{username}'\"\n        )\n        self.conn.commit()\n\n    def count_by_role(self, role: str) -> int:\n        \"\"\"Count users with a specific role.\"\"\"\n        cursor = self.conn.execute(\n            f\"SELECT COUNT(*) FROM users WHERE role = '{role}'\"\n        )\n        return cursor.fetchone()[0]\n\n    def close(self):\n        self.conn.close()\n",
    "test_user_db.py": "import sys\nfrom user_db import UserDB\n\npassed = 0\nfailed = 0\n\ndef check(desc, got, expected):\n    global passed, failed\n    if got == expected:\n        passed += 1\n    else:\n        print(f\"FAIL {desc}: got {got!r}, expected {expected!r}\")\n        failed += 1\n\n# Basic functionality tests\ndb = UserDB()\n\nid1 = db.add_user('alice', 'alice@example.com')\ncheck('add user returns id', id1, 1)\n\nid2 = db.add_user('bob', 'bob@example.com', 'admin')\ncheck('add admin', id2, 2)\n\nuser = db.get_user('alice')\ncheck('get user name', user['username'], 'alice')\ncheck('get user email', user['email'], 'alice@example.com')\ncheck('get user role', user['role'], 'user')\n\ncheck('get missing user', db.get_user('nobody'), None)\n\nresults = db.search_users('ali')\ncheck('search finds alice', len(results), 1)\ncheck('search result', results[0]['username'], 'alice')\n\nresults = db.search_users('b')\ncheck('search finds bob', len(results), 1)\n\ndb.update_role('alice', 'moderator')\nuser = db.get_user('alice')\ncheck('role updated', user['role'], 'moderator')\n\ncheck('count user role', db.count_by_role('user'), 0)\ncheck('count admin role', db.count_by_role('admin'), 1)\n\ndb.delete_user('bob')\ncheck('deleted user gone', db.get_user('bob'), None)\ncheck('alice still exists', db.get_user('alice') is not None, True)\n\ndb.close()\n\n# SQL Injection tests - these MUST be prevented\ndb2 = UserDB()\ndb2.add_user('victim', 'victim@example.com', 'user')\n\n# Test 1: Injection in username on add\ntry:\n    db2.add_user(\"evil'; DROP TABLE users; --\", 'evil@example.com')\n    # Table should still exist and be queryable\n    user = db2.get_user('victim')\n    check('table survives injection in add', user is not None, True)\nexcept Exception:\n    # An exception is acceptable — the injection was blocked\n    user = db2.get_user('victim')\n    check('table survives injection in add', user is not None, True)\n\n# Test 2: Injection in get_user\nresult = db2.get_user(\"' OR '1'='1\")\ncheck('injection in get returns None or single', result is None or result.get('username') != 'victim', True)\n\n# Test 3: Injection in search\nresults = db2.search_users(\"' OR '1'='1\")\ncheck('injection in search limited', len(results) <= 1, True)  # Should not dump all users\n\n# Test 4: Injection in update_role trying to escalate\ndb2.update_role(\"victim' OR '1'='1\", 'admin')\nvictim = db2.get_user('victim')\ncheck('role injection blocked', victim['role'], 'user')\n\n# Test 5: Injection in delete trying to delete all\ndb2.add_user('safe_user', 'safe@example.com')\ndb2.delete_user(\"nobody' OR '1'='1\")\nvictim = db2.get_user('victim')\nsafe = db2.get_user('safe_user')\ncheck('delete injection blocked victim', victim is not None, True)\ncheck('delete injection blocked safe', safe is not None, True)\n\n# Test 6: Injection in count_by_role\ncount = db2.count_by_role(\"' OR '1'='1\")\ncheck('count injection returns 0', count, 0)\n\n# Test 7: Username with special chars should work correctly\ndb2.add_user(\"o'brien\", 'obrien@example.com')\nuser = db2.get_user(\"o'brien\")\ncheck('special char username works', user is not None and user['username'] == \"o'brien\", True)\n\ndb2.close()\n\nprint(f\"{passed}/{passed+failed} tests passed\")\nif failed > 0:\n    sys.exit(1)\n"
  },
  "verify": "cd $BENCH_WORK_DIR && python3 test_user_db.py",
  "timeout": 240000,
  "tags": ["security", "python", "sql-injection", "vulnerability-fix"]
}
