[{"data":1,"prerenderedAt":103},["ShallowReactive",2],{"qa-\u002Fsql\u002Fsecurity\u002Fsql-injection":3},{"page":4,"siblings":94,"blog":100},{"id":5,"title":6,"body":7,"description":11,"difficulty":14,"extension":15,"framework":16,"frameworkSlug":17,"meta":18,"navigation":19,"order":12,"path":20,"questions":21,"questionsCount":84,"related":85,"seo":86,"seoDescription":87,"stem":88,"subtopic":89,"topic":90,"topicSlug":91,"updated":92,"__hash__":93},"qa\u002Fsql\u002Fsecurity\u002Fsql-injection.md","Sql Injection",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"medium","md","SQL","sql",{},true,"\u002Fsql\u002Fsecurity\u002Fsql-injection",[22,27,31,35,39,44,48,52,56,60,64,68,72,76,80],{"id":23,"difficulty":24,"q":25,"a":26},"what-is-sql-injection","easy","What is SQL injection?","**SQL injection** (SQLi) is an attack where an adversary inserts malicious\nSQL syntax into user-supplied input that is concatenated directly into a\nquery. The database executes the injected SQL as if it were written by the\ndeveloper.\n\n```python\n# Vulnerable: string concatenation with user input\nusername = request.get(\"username\")   # attacker provides: ' OR '1'='1\nquery = \"SELECT * FROM users WHERE username = '\" + username + \"'\"\n# Resulting SQL: SELECT * FROM users WHERE username = '' OR '1'='1'\n# → returns ALL users! The attacker is logged in without credentials.\n```\n\nConsequences range from data theft (reading all rows), authentication\nbypass, data destruction (`DROP TABLE`), to remote code execution via\ndatabase functions (`xp_cmdshell` in SQL Server).\n\n**Rule of thumb:** SQL injection is consistently in OWASP's Top 10 list\nof critical web application vulnerabilities and is 100 % preventable with\nparameterised queries. Never concatenate user input into SQL strings.\n",{"id":28,"difficulty":24,"q":29,"a":30},"parameterised-queries","What are parameterised queries (prepared statements) and why do they prevent injection?","A **parameterised query** separates the SQL structure from the data values.\nThe query is sent to the database with **placeholders**, and the driver\nsends the data values separately. The database always treats the values as\ndata — never as SQL syntax.\n\n```python\n# Python + psycopg2 (Postgres) — SAFE\ncur.execute(\n    \"SELECT * FROM users WHERE username = %s AND password_hash = %s\",\n    (username, password_hash)   # values sent separately, never interpolated\n)\n# Even if username = \"' OR '1'='1\", it is treated as a literal string,\n# not SQL. The query finds no user named \"' OR '1'='1\".\n\n# Node.js + pg — SAFE\nconst result = await client.query(\n    'SELECT * FROM users WHERE id = $1',\n    [userId]\n);\n\n# Java + JDBC — SAFE\nPreparedStatement stmt = conn.prepareStatement(\n    \"SELECT * FROM orders WHERE customer_id = ?\");\nstmt.setInt(1, customerId);\n```\n\n**Rule of thumb:** **always use parameterised queries** (also called\nprepared statements) for any query that includes user-supplied data. This\nis the single most effective prevention against SQL injection.\n",{"id":32,"difficulty":14,"q":33,"a":34},"orm-safety","Are ORM queries safe from SQL injection by default?","Most ORM frameworks (SQLAlchemy, Django ORM, ActiveRecord, Hibernate)\nuse parameterised queries by default, making their standard query API\ninjection-safe. However, raw SQL escape hatches in ORMs can re-introduce\nthe vulnerability.\n\n```python\n# Django ORM — SAFE (uses parameterised queries internally)\nUser.objects.filter(username=username)\n\n# Django raw() — UNSAFE if you concatenate input\nUser.objects.raw(f\"SELECT * FROM users WHERE username = '{username}'\")\n\n# Django raw() — SAFE with params\nUser.objects.raw(\"SELECT * FROM users WHERE username = %s\", [username])\n\n# SQLAlchemy — SAFE\nsession.execute(select(User).where(User.username == username))\n\n# SQLAlchemy text() — UNSAFE if you concatenate\nsession.execute(text(f\"SELECT * FROM users WHERE username = '{username}'\"))\n\n# SQLAlchemy text() — SAFE with bindparam\nsession.execute(text(\"SELECT * FROM users WHERE username = :u\"), {\"u\": username})\n```\n\n**Rule of thumb:** use the ORM's type-safe query API wherever possible.\nWhen you must write raw SQL, always use parameterised bindings — never\nf-strings or string concatenation.\n",{"id":36,"difficulty":14,"q":37,"a":38},"injection-types","What are the main types of SQL injection attacks?","1. **In-band SQLi** — data is extracted through the same channel as the\n   attack (most common). Includes error-based (reading error messages) and\n   union-based (appending `UNION SELECT` to leak data).\n2. **Blind SQLi** — the application does not return data but the attacker\n   infers information from behaviour:\n   - **Boolean-based**: send a true vs false condition; observe response\n     differences.\n   - **Time-based**: use `pg_sleep()` or `SLEEP()` to cause a delay if a\n     condition is true.\n3. **Out-of-band SQLi** — data is exfiltrated via a different channel\n   (DNS lookup, HTTP request) using database features like `UTL_HTTP`\n   (Oracle) or `xp_cmdshell` (SQL Server).\n\n```sql\n-- Union-based example (attacker appends):\n-- Original: SELECT name FROM products WHERE id = 1\n-- Injected: id = 1 UNION SELECT password FROM users--\n-- Result: returns product name AND user passwords\n\n-- Time-based blind example:\n-- id = 1; IF (SELECT COUNT(*) FROM users WHERE username='admin') > 0\n--           BEGIN WAITFOR DELAY '0:0:5' END--\n-- If the response is delayed 5 s, an 'admin' user exists.\n```\n\n**Rule of thumb:** parameterised queries prevent all forms of in-band and\nmost out-of-band injection. Separately, disable dangerous stored procedures\n(`xp_cmdshell`, `UTL_HTTP`) unless explicitly required.\n",{"id":40,"difficulty":41,"q":42,"a":43},"stored-procedure-injection","hard","Can stored procedures be vulnerable to SQL injection?","Yes — stored procedures that build dynamic SQL internally via string\nconcatenation are still vulnerable. The parameter is safe from injection\nat the call site, but the SQL built inside the procedure is not.\n\n```sql\n-- SQL Server stored procedure — UNSAFE (dynamic SQL with concatenation)\nCREATE PROCEDURE SearchProducts @SearchTerm NVARCHAR(100)\nAS\nBEGIN\n  EXEC('SELECT * FROM products WHERE name LIKE ''%' + @SearchTerm + '%''')\n  -- Attacker passes: '; DROP TABLE products; --\n  -- Becomes: SELECT * FROM products WHERE name LIKE '%'; DROP TABLE products; --%'\nEND;\n\n-- SAFE: use sp_executesql with parameters\nCREATE PROCEDURE SearchProducts @SearchTerm NVARCHAR(100)\nAS\nBEGIN\n  DECLARE @sql NVARCHAR(500) = N'SELECT * FROM products WHERE name LIKE @term';\n  EXEC sp_executesql @sql, N'@term NVARCHAR(102)', @term = '%' + @SearchTerm + '%';\nEND;\n```\n\n**Rule of thumb:** dynamic SQL inside stored procedures must use\n`sp_executesql` with bound parameters (SQL Server), `EXECUTE USING` with\n`$1` placeholders (Postgres PL\u002FpgSQL), or `PREPARE`\u002F`EXECUTE` equivalents.\nNever concatenate user input into a dynamic SQL string, even inside a\nstored procedure.\n",{"id":45,"difficulty":41,"q":46,"a":47},"second-order-injection","What is second-order (stored) SQL injection?","**Second-order injection** occurs in two steps:\n1. Malicious input is stored safely in the database (the initial insertion\n   is parameterised and appears safe).\n2. Later, that stored value is retrieved and concatenated into a new SQL\n   query without parameterisation — causing injection on the second use.\n\n```python\n# Step 1: user registers with username = \"admin'--\"\n# This INSERT is parameterised — safe at registration:\ncur.execute(\"INSERT INTO users (username) VALUES (%s)\", (username,))\n# username = \"admin'--\" is stored harmlessly.\n\n# Step 2: admin panel retrieves the username and uses it unsafely:\nadmin_username = fetch_user(user_id)[\"username\"]  # → \"admin'--\"\ncur.execute(f\"SELECT * FROM audit_log WHERE actor = '{admin_username}'\")\n# → SELECT * FROM audit_log WHERE actor = 'admin'--'\n# The -- comments out the rest → dumps all audit log rows\n```\n\n**Rule of thumb:** data from the database must be treated as untrusted\nwhen used in a new query — even if you stored it safely. Always use\nparameterised queries for every database query, including queries that\nuse data retrieved from the database itself.\n",{"id":49,"difficulty":14,"q":50,"a":51},"input-validation","Is input validation sufficient to prevent SQL injection?","Input validation is a **useful defence-in-depth measure** but is **not\nsufficient on its own** to prevent SQL injection. Allowlists (accepting\nonly known-good patterns) are more reliable than denylists (rejecting\nknown-bad strings), but both can be bypassed by clever encoding or\nunexpected input formats.\n\n```python\n# Denylist — INSUFFICIENT (easily bypassed with encoding)\nif \"'\" in user_input or \";\" in user_input:\n    raise ValueError(\"Invalid input\")\n# Attacker uses URL encoding, Unicode lookalikes, or multi-byte tricks to bypass\n\n# Allowlist — better but still not sufficient alone\nimport re\nif not re.match(r'^[a-zA-Z0-9_]+$', username):\n    raise ValueError(\"Invalid username\")\n# Better — but parameterised queries are STILL required as the primary defence\n```\n\nThe correct stack:\n1. **Parameterised queries** — primary defence (mandatory)\n2. **Input validation\u002Fallowlists** — secondary, reduces attack surface\n3. **Least privilege** — limits blast radius if injection occurs\n4. **WAF** — tertiary, may block some automated scans\n\n**Rule of thumb:** validate inputs AND use parameterised queries. Input\nvalidation is not a substitute for parameterisation — it is an additional\nlayer. If you have to choose one, choose parameterised queries.\n",{"id":53,"difficulty":14,"q":54,"a":55},"error-messages","How do database error messages contribute to SQL injection risk?","**Verbose database error messages** expose the query structure, table names,\ncolumn names, and database version to an attacker — information used to\nrefine an injection attack (error-based SQLi).\n\n```python\n# BAD: returning the raw database error to the client\nexcept Exception as e:\n    return {\"error\": str(e)}\n# Attacker sees: 'column \"passwrd\" does not exist' → typo in column name revealed\n# Or: 'relation \"users\" does not exist' → table name confirmed\n\n# GOOD: log the full error server-side; return a generic message to the client\nimport logging\nexcept Exception as e:\n    logging.error(\"Database error: %s\", e, exc_info=True)\n    return {\"error\": \"An internal error occurred. Please try again.\"}\n```\n\n**Rule of thumb:** never expose raw database error messages to end users.\nLog them server-side with full stack traces and return a generic \"internal\nerror\" to the client. Use different log levels (DEBUG in development, ERROR\nin production) to ensure errors are visible to developers but not attackers.\n",{"id":57,"difficulty":14,"q":58,"a":59},"waf-and-defence-in-depth","What role does a Web Application Firewall (WAF) play in preventing SQL injection?","A **WAF** inspects HTTP requests and blocks patterns that look like SQL\ninjection attempts (quotes, SQL keywords in unusual positions, encoded\npayloads). It provides a useful additional layer but should not be the\nprimary defence.\n\nLimitations of WAFs:\n- They can be bypassed with obfuscation (encoding, case variation, comments).\n- They may produce false positives, blocking legitimate requests.\n- They do nothing for second-order injection (the attack comes from the\n  database, not HTTP).\n- They are a perimeter control — if bypassed, no protection remains.\n\n```\nDefence-in-depth layers (innermost = most important):\n4. WAF             — blocks automated scans, buys time\n3. Input validation — reduces attack surface\n2. Least privilege  — limits blast radius\n1. Parameterised queries — PRIMARY DEFENCE (cannot be bypassed)\n```\n\n**Rule of thumb:** deploy a WAF as a defence-in-depth measure, not as a\nsubstitute for parameterised queries. A WAF buys you protection against\nautomated tools and script kiddies; a determined attacker will bypass it.\n",{"id":61,"difficulty":41,"q":62,"a":63},"nosql-injection","Does SQL injection also apply to NoSQL databases?","SQL injection is specific to SQL databases, but analogous **NoSQL injection**\nattacks exist. MongoDB, for example, is vulnerable to operator injection when\nuser input is used directly in a query object.\n\n```javascript\n\u002F\u002F MongoDB — UNSAFE: user controls the query operator\nconst username = req.body.username;  \u002F\u002F attacker sends: { \"$ne\": null }\nconst user = await User.findOne({ username: username });\n\u002F\u002F Becomes: db.users.findOne({ username: { $ne: null } })\n\u002F\u002F → returns the FIRST user in the collection, bypassing login!\n\n\u002F\u002F SAFE: validate that username is a string before using it\nif (typeof username !== 'string') throw new Error('Invalid input');\nconst user = await User.findOne({ username: username });\n```\n\nThe prevention principle is the same: **never allow untrusted input to\ncontrol query structure**. In MongoDB, validate types strictly; in Redis,\nnever concatenate user input into Lua scripts.\n\n**Rule of thumb:** the injection principle extends beyond SQL — any query\nlanguage that mixes structure and data is potentially vulnerable when user\ninput influences the structure. Validate types and use library-provided\nsafe query builders for every database technology.\n",{"id":65,"difficulty":14,"q":66,"a":67},"orm-mass-assignment","What is mass assignment and how does it relate to database security?","**Mass assignment** is not SQL injection, but is an ORM-related vulnerability\nwhere an attacker sets database columns they should not control by sending\nextra fields in an HTTP request body.\n\n```python\n# Django — VULNERABLE to mass assignment\n# Attacker sends POST: { \"username\": \"alice\", \"is_admin\": true }\nuser = User(**request.POST.dict())  # copies ALL fields including is_admin!\nuser.save()\n\n# SAFE: use an explicit allowlist of fields\nuser = User(\n    username=request.POST['username'],\n    email=request.POST['email'],\n    # is_admin NOT included — cannot be set by the user\n)\n\n# Django Forms provide this automatically:\nform = UserRegistrationForm(request.POST)  # only processes declared fields\nif form.is_valid():\n    form.save()\n```\n\n**Rule of thumb:** never pass raw request data directly to ORM constructors\nor `update()` calls. Always explicitly allowlist the fields that users are\npermitted to set, and never expose internal fields like `is_admin`,\n`role`, or `account_balance` to user-controlled input.\n",{"id":69,"difficulty":14,"q":70,"a":71},"detecting-sqli","How do you detect SQL injection vulnerabilities in an existing codebase?","```python\n# 1. Code review: grep for string interpolation into SQL\n# Dangerous patterns in Python:\n# f\"SELECT ... {user_input}\"\n# \"SELECT ... \" + variable\n# \"SELECT ... %s\" % variable   ← % formatting bypasses parameterisation!\n# cursor.execute(\"... \" + x)\n\n# 2. Automated scanning tools:\n# - sqlmap (black-box: tests a live endpoint for injection)\n# - Bandit (Python SAST: flags unsafe DB calls)\n# - Semgrep rules for SQL injection patterns\n# - OWASP ZAP (web app scanner)\n\n# 3. Database query logs: look for queries with unusual quoting\n# Postgres: log_min_duration_statement = 0 + pg_stat_statements\n\n# 4. Unit tests for injection payloads\ndef test_no_injection():\n    result = search_products(\"' OR '1'='1\")\n    assert len(result) == 0  # should find nothing, not all products\n```\n\n**Rule of thumb:** include injection payload tests in your test suite for\nevery query that accepts user input. Run `sqlmap` or a similar scanner\nagainst staging environments before release. Add a Semgrep or Bandit check\nto CI to catch string concatenation into SQL at code-review time.\n",{"id":73,"difficulty":41,"q":74,"a":75},"dynamic-order-by-injection","How do you safely handle a dynamic ORDER BY clause?","`ORDER BY` column names and directions cannot be passed as bind parameters\n— only values can. This means dynamic sorting is a common injection vector\nwhen developers concatenate the sort column directly from user input.\n\n```python\n# UNSAFE: attacker controls column name\ncolumn = request.args.get(\"sort\")  # attacker sends: \"1; DROP TABLE orders; --\"\ncur.execute(f\"SELECT * FROM orders ORDER BY {column}\")  # injection!\n\n# SAFE: allowlist of permitted column names\nALLOWED_SORT_COLUMNS = {\"id\", \"created_at\", \"total\", \"status\"}\nALLOWED_DIRECTIONS  = {\"ASC\", \"DESC\"}\n\ncolumn    = request.args.get(\"sort\",      \"created_at\")\ndirection = request.args.get(\"direction\", \"DESC\").upper()\n\nif column not in ALLOWED_SORT_COLUMNS:\n    column = \"created_at\"   # fall back to safe default\nif direction not in ALLOWED_DIRECTIONS:\n    direction = \"DESC\"\n\n# Now safe to interpolate — values are from a known-good set\ncur.execute(f\"SELECT * FROM orders ORDER BY {column} {direction}\")\n```\n\n**Rule of thumb:** for any dynamic SQL identifier (column name, table name,\nschema name), use an explicit allowlist — never accept the raw user value.\nBind parameters cannot protect identifiers, only values.\n",{"id":77,"difficulty":14,"q":78,"a":79},"escape-vs-parameterise","What is the difference between escaping and parameterising?","**Escaping** modifies the input string to neutralise special characters\n(e.g., replacing `'` with `''`) before interpolating it into SQL.\n**Parameterising** sends the SQL structure and data values to the database\nas separate payloads — the driver handles quoting internally.\n\n```python\n# Escaping — FRAGILE (easily bypassed with multi-byte character tricks)\nname = user_input.replace(\"'\", \"''\")\ncur.execute(f\"SELECT * FROM users WHERE name = '{name}'\")\n\n# Parameterising — CORRECT\ncur.execute(\"SELECT * FROM users WHERE name = %s\", (user_input,))\n```\n\nWhy escaping is unreliable:\n- Requires knowing all dangerous characters for the current charset.\n- Multi-byte encodings (GBK, BIG5) can hide a `'` byte inside a two-byte\n  sequence, making `replace(\"'\", \"''\")` ineffective.\n- A single missed escape anywhere in a large codebase is a vulnerability.\n\n**Rule of thumb:** never escape and interpolate — always parameterise.\nEscaping is a last resort when a driver or ORM provides no parameterisation\noption and you must write raw SQL. Even then, use the driver's official\nescaping function (`psycopg2.extensions.adapt`, `mysqli_real_escape_string`)\n— never roll your own.\n",{"id":81,"difficulty":14,"q":82,"a":83},"least-privilege-sqli","How does least-privilege database access reduce SQL injection impact?","If injection does occur despite parameterisation (e.g., via a legacy code\npath), **least privilege** limits the blast radius — the attacker can only\ndo what the compromised database account can do.\n\n```sql\n-- BAD: application uses a superuser or DBA account\n-- Attacker can: DROP TABLE, read pg_shadow (password hashes), run COPY TO,\n--               call xp_cmdshell (SQL Server), access all schemas\n\n-- GOOD: application uses a restricted role\nCREATE ROLE app_runtime NOINHERIT;\nGRANT SELECT, INSERT, UPDATE, DELETE ON orders    TO app_runtime;\nGRANT SELECT, INSERT, UPDATE, DELETE ON customers TO app_runtime;\nGRANT SELECT ON products TO app_runtime;\n-- NOT granted: DROP TABLE, ALTER, TRUNCATE, CREATE, COPY TO\u002FFROM\n-- NOT granted: pg_read_file, pg_execute_server_program\n-- NOT granted: access to other schemas\n\n-- Even with injection, attacker can only DML on those three tables\n```\n\n**Rule of thumb:** least privilege is not a substitute for parameterised\nqueries, but it is an essential backstop. A successful injection against\na read-only reporting account is far less damaging than one against a DBA\naccount. Always pair both controls.\n",15,null,{"description":11},"SQL injection interview questions — how attacks work, parameterised queries, ORM safety, stored procedure risks, second-order injection, blind injection, WAFs, and prevention best practices.","sql\u002Fsecurity\u002Fsql-injection","SQL Injection","Security & Integrity","security","2026-06-20","-Q1GM9d1hBs2zfZExPTxntUlmwQfL0ra-HAaWbczZIE",[95,99],{"subtopic":96,"path":97,"order":98},"Permissions & Roles","\u002Fsql\u002Fsecurity\u002Fpermissions",1,{"subtopic":89,"path":20,"order":12},{"path":101,"title":102},"\u002Fblog\u002Fsql-injection-prevention","SQL Injection — How It Works and How to Prevent It",1782244107567]