[{"data":1,"prerenderedAt":187},["ShallowReactive",2],{"topic-sql-security":3},{"framework":4,"topic":16,"subtopics":25},{"id":5,"description":6,"extension":7,"icon":8,"meta":9,"name":10,"order":11,"slug":12,"stem":13,"tier":14,"__hash__":15},"frameworks\u002Fframeworks\u002Fsql.yml","SQL interview questions on queries, joins and aggregation — essential for every backend, data and analytics interview.","yml","database",{},"SQL",4,"sql","frameworks\u002Fsql",1,"lpzsOj2p9p9W0Tctwc61nP-ulZAA80R5gJiyaZS6ZeI",{"id":17,"description":18,"extension":7,"frameworkSlug":12,"meta":19,"name":20,"order":21,"slug":22,"stem":23,"__hash__":24},"topics\u002Ftopics\u002Fsql-security.yml","GRANT\u002FREVOKE, roles and SQL injection prevention — controlling who can do what and keeping queries safe from untrusted input.",{},"Security & Integrity",9,"security","topics\u002Fsql-security","CulSzfQJU1lvOsDtTE05Ikcb4-IEzgFAZpFzlff0ON0",[26,112],{"id":27,"title":28,"body":29,"description":33,"difficulty":36,"extension":37,"framework":10,"frameworkSlug":12,"meta":38,"navigation":39,"order":14,"path":40,"questions":41,"questionsCount":104,"related":105,"seo":106,"seoDescription":107,"stem":108,"subtopic":109,"topic":20,"topicSlug":22,"updated":110,"__hash__":111},"qa\u002Fsql\u002Fsecurity\u002Fpermissions.md","Permissions",{"type":30,"value":31,"toc":32},"minimark",[],{"title":33,"searchDepth":34,"depth":34,"links":35},"",2,[],"medium","md",{},true,"\u002Fsql\u002Fsecurity\u002Fpermissions",[42,47,51,55,59,64,68,72,76,80,84,88,92,96,100],{"id":43,"difficulty":44,"q":45,"a":46},"grant-revoke","easy","What do GRANT and REVOKE do?","`GRANT` gives a database user or role permission to perform an operation.\n`REVOKE` removes a previously granted permission.\n\n```sql\n-- Grant SELECT on one table\nGRANT SELECT ON orders TO analyst_user;\n\n-- Grant multiple privileges at once\nGRANT SELECT, INSERT, UPDATE ON products TO app_user;\n\n-- Grant on all tables in a schema (Postgres)\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;\n\n-- Grant to a role (not a user directly)\nGRANT SELECT ON customers TO reporting_role;\n\n-- Revoke a specific privilege\nREVOKE INSERT ON products FROM app_user;\n\n-- Revoke all privileges on a table\nREVOKE ALL PRIVILEGES ON orders FROM analyst_user;\n```\n\n**Rule of thumb:** always grant to **roles**, not individual users. Assign\nusers to roles. This makes permission management scalable — add a new\nemployee by assigning them to the correct role, not by running a dozen\n`GRANT` statements.\n",{"id":48,"difficulty":44,"q":49,"a":50},"least-privilege","What is the principle of least privilege and how does it apply to SQL?","**Least privilege** means giving each user or service account only the\nminimum database permissions needed to perform its function — no more.\n\n```sql\n-- Application database user: only needs to read\u002Fwrite its own tables\nCREATE ROLE app_role;\nGRANT SELECT, INSERT, UPDATE, DELETE ON orders    TO app_role;\nGRANT SELECT, INSERT, UPDATE, DELETE ON customers TO app_role;\nGRANT SELECT ON products TO app_role;  -- read-only on the product catalogue\n-- NOT granted: DROP TABLE, CREATE TABLE, TRUNCATE, ALTER\n\n-- Reporting user: read-only access\nCREATE ROLE reporting_role;\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO reporting_role;\n\n-- Migrations user: only runs during deploys\nCREATE ROLE migration_role;\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO migration_role;\nGRANT CREATE ON SCHEMA public TO migration_role;\n```\n\n**Rule of thumb:** the application's runtime database user should never\nhave `DROP TABLE`, `TRUNCATE`, or `ALTER TABLE` privileges. Use a separate\nmigration user for schema changes, and revoke it after deploys.\n",{"id":52,"difficulty":36,"q":53,"a":54},"roles","What are roles and how do they differ from users?","A **role** is a named collection of privileges. A **user** is a role that\ncan log in. In Postgres, users and roles are unified — `CREATE USER` is\nsyntactic sugar for `CREATE ROLE … LOGIN`.\n\n```sql\n-- Postgres: create roles\nCREATE ROLE readonly_role;\nCREATE ROLE readwrite_role;\nCREATE ROLE admin_role;\n\n-- Grant privileges to roles\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;\nGRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO readwrite_role;\n\n-- Create users (roles that can log in)\nCREATE ROLE alice LOGIN PASSWORD 'secret';\nCREATE ROLE bob   LOGIN PASSWORD 'secret';\n\n-- Assign users to roles\nGRANT readonly_role  TO alice;\nGRANT readwrite_role TO bob;\n\n-- Alice now inherits all privileges of readonly_role\n```\n\nSQL Server uses `CREATE LOGIN` (server-level) + `CREATE USER` (database-level)\n+ `CREATE ROLE` as separate concepts.\n\n**Rule of thumb:** define a small set of roles that match your access\npatterns (read-only, read-write, admin, migration). Add users to roles\nrather than granting privileges to individual users — it is far easier to\naudit and maintain.\n",{"id":56,"difficulty":36,"q":57,"a":58},"grant-with-grant-option","What does WITH GRANT OPTION do?","`WITH GRANT OPTION` allows the grantee to re-grant the same privilege\nto other users or roles.\n\n```sql\n-- Alice can SELECT on orders AND can grant that to others\nGRANT SELECT ON orders TO alice WITH GRANT OPTION;\n\n-- Alice can now do:\nGRANT SELECT ON orders TO bob;  -- valid because alice has GRANT OPTION\n\n-- Revoke cascades to anyone alice granted to\nREVOKE SELECT ON orders FROM alice CASCADE;\n-- Bob loses SELECT too, because it came from alice\n```\n\n`WITH GRANT OPTION` creates a chain of trust that is hard to audit — Bob's\naccess depends on Alice's access, and revoking Alice's access removes Bob's.\n\n**Rule of thumb:** avoid `WITH GRANT OPTION` in most cases — it makes\npermission chains hard to audit and revocations unpredictable. Only use it\nfor schema owners or DBA roles that are explicitly responsible for managing\naccess.\n",{"id":60,"difficulty":61,"q":62,"a":63},"row-level-security","hard","What is Row-Level Security (RLS) in Postgres?","**Row-Level Security (RLS)** enforces access policies at the table level —\nthe database automatically filters rows based on the current user, hiding\nrows that the policy says the user cannot see.\n\n```sql\n-- Enable RLS on the table\nALTER TABLE orders ENABLE ROW LEVEL SECURITY;\n\n-- Policy: users can only see their own orders\nCREATE POLICY own_orders ON orders\n  FOR ALL\n  TO app_role\n  USING (customer_id = current_setting('app.current_user_id')::INT);\n\n-- Application sets the config before every query\nSET app.current_user_id = '42';\nSELECT * FROM orders;  -- automatically filtered to customer_id = 42\n\n-- Bypass RLS (table owner and superuser bypass by default)\nALTER TABLE orders FORCE ROW LEVEL SECURITY;  -- forces even for table owner\n```\n\n**Rule of thumb:** use RLS for multi-tenant applications where every table\nquery must be tenant-scoped. It enforces the isolation at the database level\n— a bug in application code cannot accidentally expose another tenant's data.\n",{"id":65,"difficulty":36,"q":66,"a":67},"schema-permissions","How do schema-level permissions work?","In Postgres, a user must have `USAGE` on a **schema** before they can access\nany objects within it, even if they have `SELECT` on individual tables.\n\n```sql\n-- Step 1: grant schema access\nGRANT USAGE ON SCHEMA reporting TO analyst_role;\n\n-- Step 2: grant table access within the schema\nGRANT SELECT ON ALL TABLES IN SCHEMA reporting TO analyst_role;\n\n-- Ensure future tables are also covered (Postgres)\nALTER DEFAULT PRIVILEGES IN SCHEMA reporting\n  GRANT SELECT ON TABLES TO analyst_role;\n\n-- Deny schema access entirely\nREVOKE USAGE ON SCHEMA private_data FROM analyst_role;\n-- Now analyst_role cannot see any tables in private_data schema\n```\n\n**Rule of thumb:** use separate schemas (`app`, `reporting`, `audit`,\n`staging`) and grant schema `USAGE` per role. This lets you grant broad\naccess to an entire schema with two `GRANT` statements instead of one per\ntable.\n",{"id":69,"difficulty":36,"q":70,"a":71},"column-level-grants","Can you grant permissions on specific columns rather than whole tables?","Yes — SQL supports column-level `SELECT`, `INSERT`, and `UPDATE` grants.\nThis lets you expose some columns of a sensitive table while hiding others\n(e.g., show names but not salaries).\n\n```sql\n-- Grant SELECT on specific columns only\nGRANT SELECT (id, name, department) ON employees TO hr_report_role;\n-- hr_report_role can NOT read salary or ssn columns\n\n-- Grant UPDATE on specific columns\nGRANT UPDATE (email, phone) ON users TO support_role;\n-- support_role can update contact info but not password_hash\n\n-- Revoke column-level grant\nREVOKE SELECT (salary) ON employees FROM payroll_role;\n```\n\nColumn-level grants work in Postgres, SQL Server, and MySQL but are\ncomplex to manage. An alternative is to create a **view** that exposes\nonly the allowed columns and grant `SELECT` on the view instead.\n\n**Rule of thumb:** prefer a view over column-level grants for hiding\nsensitive columns — views are easier to discover, test, and document.\nUse column-level grants when a view is not practical (e.g., you need\nwrite permissions on specific columns).\n",{"id":73,"difficulty":36,"q":74,"a":75},"superuser-privileges","What are superuser privileges and why should you avoid using them for applications?","A **superuser** (Postgres) or **sysadmin** (SQL Server) bypasses all\npermission checks and can do anything in the database — create\u002Fdrop\ndatabases, bypass RLS, read any table, impersonate other users.\n\n```sql\n-- Check if current user is a superuser (Postgres)\nSELECT current_user, usesuper FROM pg_user WHERE usename = current_user;\n\n-- Create a non-superuser admin for routine work\nCREATE ROLE dba_role CREATEDB CREATEROLE;  -- can manage DBs and roles, not superuser\n\n-- Application connection string should NEVER use a superuser\n-- BAD:  postgresql:\u002F\u002Fpostgres:password@host\u002Fdb\n-- GOOD: postgresql:\u002F\u002Fapp_user:password@host\u002Fdb (limited privileges)\n```\n\n**Rule of thumb:** the application's database connection string must never\nuse a superuser account. Use superuser credentials only for database\nadministration tasks, run from a secured bastion host or local machine —\nnever from application servers.\n",{"id":77,"difficulty":61,"q":78,"a":79},"audit-logging","How do you audit who changed what in a database?","Audit logging records which user performed which action and when. Common\napproaches:\n\n1. **Application-level**: record the actor and action in an audit table\n   from application code.\n2. **Trigger-based**: a database trigger automatically writes to an audit\n   table on every `INSERT`\u002F`UPDATE`\u002F`DELETE`.\n3. **Extension\u002Ffeature**: `pgaudit` (Postgres), SQL Server Audit, MySQL\n   General Log.\n\n```sql\n-- Postgres: trigger-based audit log\nCREATE TABLE audit_log (\n  id         BIGSERIAL PRIMARY KEY,\n  table_name TEXT NOT NULL,\n  operation  TEXT NOT NULL,  -- INSERT \u002F UPDATE \u002F DELETE\n  old_data   JSONB,\n  new_data   JSONB,\n  changed_by TEXT NOT NULL DEFAULT current_user,\n  changed_at TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\nCREATE OR REPLACE FUNCTION audit_trigger() RETURNS TRIGGER AS $$\nBEGIN\n  INSERT INTO audit_log (table_name, operation, old_data, new_data)\n  VALUES (TG_TABLE_NAME, TG_OP,\n          CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD)::jsonb END,\n          CASE WHEN TG_OP \u003C> 'DELETE' THEN row_to_json(NEW)::jsonb END);\n  RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nCREATE TRIGGER orders_audit\n  AFTER INSERT OR UPDATE OR DELETE ON orders\n  FOR EACH ROW EXECUTE FUNCTION audit_trigger();\n```\n\n**Rule of thumb:** trigger-based auditing is comprehensive but adds write\nlatency. For compliance-grade auditing, use a dedicated extension\n(`pgaudit`) or the database's native audit feature — they capture more\nevents (reads, DDL) and cannot be bypassed by application code that\ncircumvents triggers.\n",{"id":81,"difficulty":61,"q":82,"a":83},"revoking-public-schema","Why is the PUBLIC schema dangerous in Postgres and how do you secure it?","In Postgres, every user has `CREATE` and `USAGE` on the `public` schema by\ndefault (before Postgres 15). Any user can create tables there, potentially\n**shadowing** system functions or other users' objects via the search path.\n\n```sql\n-- Check who has what on the public schema\nSELECT grantee, privilege_type\nFROM   information_schema.role_schema_grants\nWHERE  schema_name = 'public';\n\n-- Revoke CREATE from all non-superusers (Postgres \u003C 15)\nREVOKE CREATE ON SCHEMA public FROM PUBLIC;\n\n-- Postgres 15+: CREATE is revoked from PUBLIC by default\n-- but USAGE is still granted — revoke if needed\nREVOKE USAGE ON SCHEMA public FROM PUBLIC;\n\n-- Application code should use an explicit schema, not rely on search_path\nSET search_path = app, public;\n-- Or set it per user:\nALTER ROLE app_user SET search_path = app;\n```\n\n**Rule of thumb:** on a shared database, immediately revoke `CREATE ON\nSCHEMA public FROM PUBLIC` (Postgres \u003C 15) and put application objects in\na dedicated schema. Set `search_path` explicitly for each role to prevent\nsearch-path hijacking attacks.\n",{"id":85,"difficulty":36,"q":86,"a":87},"password-policies","How do you manage database user passwords securely?","```sql\n-- Postgres: create a user with a password\nCREATE ROLE app_user LOGIN PASSWORD 'str0ng-p@ssw0rd!';\n\n-- Set password expiry (force rotation)\nALTER ROLE app_user VALID UNTIL '2026-12-31';\n\n-- Use SCRAM-SHA-256 authentication (more secure than md5)\n-- In pg_hba.conf: host all all 0.0.0.0\u002F0 scram-sha-256\n-- Then:\nSET password_encryption = 'scram-sha-256';\nALTER ROLE app_user PASSWORD 'new-password';\n\n-- MySQL: create user with strong auth plugin\nCREATE USER 'app_user'@'%' IDENTIFIED WITH caching_sha2_password BY 'str0ng-p@ss';\n\n-- SQL Server: enforce password policy (Windows policy integration)\nCREATE LOGIN app_login WITH PASSWORD = 'str0ng-p@ss!',\n  CHECK_POLICY = ON, CHECK_EXPIRATION = ON;\n```\n\n**Rule of thumb:** use randomly generated, long passwords (32+ characters)\nfor service accounts and store them in a secrets manager (Vault, AWS Secrets\nManager). Rotate passwords automatically. Never hardcode credentials in\napplication source code.\n",{"id":89,"difficulty":36,"q":90,"a":91},"connection-security","How do you restrict which hosts can connect to the database?","Database-level host restrictions add a network layer of access control —\neven if credentials are compromised, connections from unauthorised IPs are\nrejected.\n\n```sql\n-- Postgres: pg_hba.conf (host-based authentication file)\n-- Each line: TYPE  DATABASE  USER  ADDRESS  METHOD\n-- Allow the app server IP only:\nhost   myapp    app_user   10.0.1.5\u002F32    scram-sha-256\n-- Reject everything else:\nhost   myapp    all        0.0.0.0\u002F0      reject\n\n-- MySQL: user accounts include the host\nCREATE USER 'app_user'@'10.0.1.5' IDENTIFIED BY 'password';\n-- This account can ONLY connect from 10.0.1.5\nGRANT ALL ON myapp.* TO 'app_user'@'10.0.1.5';\n\n-- SQL Server: use firewall rules (Azure Portal \u002F Windows Firewall)\n-- plus Windows authentication or IP restrictions in network config\n```\n\n**Rule of thumb:** never expose the database port to the public internet.\nAllow connections only from application servers and VPN\u002Fbastion hosts,\nusing IP allowlists at both the database (`pg_hba.conf`) and network\n(firewall) levels.\n",{"id":93,"difficulty":36,"q":94,"a":95},"view-as-security","How can views be used to implement access control?","By granting access to a **view** instead of the base table, you restrict\nwhat data a user can see — without row-level security policies or column\ngrants.\n\n```sql\n-- Base table: all employees including salary and SSN\n-- View: only expose name, department, and hire date\nCREATE VIEW employee_directory AS\n  SELECT id, full_name, department, hire_date\n  FROM   employees\n  WHERE  terminated_at IS NULL;\n\n-- Grant SELECT on the view only\nGRANT SELECT ON employee_directory TO hr_partner_role;\nREVOKE ALL ON employees FROM hr_partner_role;  -- no direct table access\n\n-- HR partner can now run:\nSELECT * FROM employee_directory WHERE department = 'Engineering';\n-- Cannot see salary, SSN, or terminated employees\n```\n\n**Rule of thumb:** use views as a security layer for read-only access to\nsensitive tables when you want the constraint to be declarative and\nself-documenting. For write-access scenarios, combine views with `INSTEAD\nOF` triggers or handle mutations directly on the restricted columns.\n",{"id":97,"difficulty":36,"q":98,"a":99},"default-privileges","What are default privileges and why do they matter?","**Default privileges** define the permissions automatically applied to\nfuture objects (tables, sequences, functions) created in a schema. Without\nthem, a role that has `SELECT` on all current tables loses access as soon as\na new table is created.\n\n```sql\n-- Postgres: set default privileges for future tables in a schema\n-- Run this as the schema owner:\nALTER DEFAULT PRIVILEGES IN SCHEMA app\n  GRANT SELECT ON TABLES TO readonly_role;\n\nALTER DEFAULT PRIVILEGES IN SCHEMA app\n  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO readwrite_role;\n\nALTER DEFAULT PRIVILEGES IN SCHEMA app\n  GRANT USAGE, SELECT ON SEQUENCES TO readwrite_role;\n\n-- Verify current default privileges\nSELECT * FROM pg_default_acl;\n\n-- MySQL: no native default privileges; use a provisioning script or IAM\n```\n\n**Rule of thumb:** set `ALTER DEFAULT PRIVILEGES` for every role when you\nfirst create the schema. Without it, each new table deployed in a migration\nrequires a separate `GRANT` — easy to forget, causing silent 403s in\nproduction.\n",{"id":101,"difficulty":36,"q":102,"a":103},"privilege-inspection","How do you inspect what permissions a user or role currently has?","```sql\n-- Postgres: check table-level privileges\nSELECT grantee, table_name, privilege_type\nFROM   information_schema.role_table_grants\nWHERE  grantee = 'analyst_role'\nORDER  BY table_name;\n\n-- Postgres: psql shorthand\n-- \\dp orders           → show ACL for the orders table\n-- \\du analyst_role     → show role attributes and memberships\n-- \\z                   → show ACLs for all tables\n\n-- Postgres: check schema privileges\nSELECT grantee, schema_name, privilege_type\nFROM   information_schema.role_schema_grants\nWHERE  grantee = 'analyst_role';\n\n-- MySQL\nSHOW GRANTS FOR 'app_user'@'%';\n\n-- SQL Server\nSELECT principal_name, object_name, permission_name, state_desc\nFROM   sys.database_permissions dp\nJOIN   sys.database_principals  pr ON dp.grantee_principal_id = pr.principal_id\nWHERE  pr.name = 'analyst_role';\n```\n\n**Rule of thumb:** before revoking or changing permissions, always inspect\nthe current state first. In Postgres, `\\dp \u003Ctablename>` is the fastest\nway to spot unexpected `PUBLIC` grants on sensitive tables.\n",15,null,{"description":33},"SQL permissions interview questions — GRANT, REVOKE, roles, least-privilege, row-level security, column-level grants, schema ownership, and access control patterns across Postgres, MySQL, and SQL Server.","sql\u002Fsecurity\u002Fpermissions","Permissions & Roles","2026-06-20","HEpF1IbUqXS2me_x56LuGaV5k4-5e2wf_XpPYrAOBv8",{"id":113,"title":114,"body":115,"description":33,"difficulty":36,"extension":37,"framework":10,"frameworkSlug":12,"meta":119,"navigation":39,"order":34,"path":120,"questions":121,"questionsCount":104,"related":105,"seo":182,"seoDescription":183,"stem":184,"subtopic":185,"topic":20,"topicSlug":22,"updated":110,"__hash__":186},"qa\u002Fsql\u002Fsecurity\u002Fsql-injection.md","Sql Injection",{"type":30,"value":116,"toc":117},[],{"title":33,"searchDepth":34,"depth":34,"links":118},[],{},"\u002Fsql\u002Fsecurity\u002Fsql-injection",[122,126,130,134,138,142,146,150,154,158,162,166,170,174,178],{"id":123,"difficulty":44,"q":124,"a":125},"what-is-sql-injection","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":127,"difficulty":44,"q":128,"a":129},"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":131,"difficulty":36,"q":132,"a":133},"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":135,"difficulty":36,"q":136,"a":137},"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":139,"difficulty":61,"q":140,"a":141},"stored-procedure-injection","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":143,"difficulty":61,"q":144,"a":145},"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":147,"difficulty":36,"q":148,"a":149},"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":151,"difficulty":36,"q":152,"a":153},"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":155,"difficulty":36,"q":156,"a":157},"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":159,"difficulty":61,"q":160,"a":161},"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":163,"difficulty":36,"q":164,"a":165},"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":167,"difficulty":36,"q":168,"a":169},"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":171,"difficulty":61,"q":172,"a":173},"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":175,"difficulty":36,"q":176,"a":177},"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":179,"difficulty":36,"q":180,"a":181},"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",{"description":33},"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","-Q1GM9d1hBs2zfZExPTxntUlmwQfL0ra-HAaWbczZIE",1782244099109]