Security Best Practices

If you've used MySQL or PostgreSQL, you're used to CREATE USER, GRANT, and connecting with a username and password. SQLite has none of that — no users, no privileges, no authentication of any kind, built into the engine itself. That's not an oversight; it's a direct consequence of what SQLite is. This lesson covers what "security" actually means for a database with no login system, using RecipeBox as the running example.

Why SQLite Has No User System

MySQL and PostgreSQL are client-server databases: a separate server process listens on a network port, and clients connect to it, so the server needs a way to decide who's allowed to connect and what they're allowed to do. SQLite is embedded — it's a library linked directly into your application process, reading and writing a file on the same machine. There's no network listener and no separate process to authenticate to. Whoever can open the file is the authenticated party, as far as SQLite itself is concerned.

 MySQL / PostgreSQLSQLite
Who can connectControlled by CREATE USER + authenticationAnyone who can open the file
What they can doControlled by GRANT privileges per userWhatever the file's OS permissions allow — all or nothing
Where enforcement happensInside the database serverNowhere — it's delegated entirely to the operating system
Note: This is covered in depth from the other direction in SQLite vs MySQL vs PostgreSQL, later in this track — it's one of the defining trade-offs between an embedded and a client-server database, not a missing feature.

1. File-System Permissions Are Your Access Control

Since SQLite delegates access control entirely to the operating system, securing recipebox.db means setting normal file permissions on it, the same as any other sensitive file:

  • The .db file (and, in WAL mode, its -wal and -shm companions) should be readable and writable only by the OS user the RecipeBox application actually runs as
  • It should never live in a publicly readable location — a web server's document root, a public upload directory, or anywhere else an unrelated request could fetch it directly
  • If RecipeBox is a mobile app, this is largely handled for you: Android and iOS both sandbox each app's local database file away from other apps by default

2. SQL Injection Prevention — Just as Real for SQLite

SQLite has no privilege system to blame for a breach, which makes the application layer the entire line of defense. SQL injection — building a query by concatenating raw user input into the SQL string — is exactly as dangerous against SQLite as it is against MySQL or PostgreSQL. There's no user-based sandboxing to fall back on if it goes wrong.

-- UNSAFE: string concatenation, shown language-agnostically
-- query = "SELECT * FROM recipes WHERE title = '" + userInput + "'"
-- If userInput is:  ' OR '1'='1
SELECT * FROM recipes WHERE title = '' OR '1'='1;
-- every recipe from every user is now returned, not just a matching title
-- SAFE: a parameterized query, shown language-agnostically
-- the ? is bound to userInput by the driver, never pasted into the SQL text
SELECT * FROM recipes WHERE title = ?;

Every language binding for SQLite — Python's sqlite3, PHP's PDO, Node's better-sqlite3, Java/Android's SQLite APIs — supports parameterized queries or prepared statements. The next lesson, Connecting from Application Code, shows the exact syntax for several of them. The rule is simple: user input is always passed as a bound parameter, never pasted directly into a SQL string, no matter which language is doing the talking.

3. Be Careful with Attacker-Influenced ATTACH Paths

The ATTACH DATABASE statement opens another database file and makes it available inside the current connection, using whatever file path you give it. If that path is ever built from untrusted input, an attacker could potentially point it somewhere they shouldn't — attaching a file outside RecipeBox's own data directory, or even creating a new file at a path they control.

-- Dangerous if `userSuppliedPath` is not fully controlled by your own code
-- ATTACH DATABASE userSuppliedPath AS extra;

-- Safer: only ever ATTACH fixed, hardcoded paths your application defines itself
ATTACH DATABASE 'archive/recipebox_archive.db' AS archive;

The practical rule: never build an ATTACH path (or any other file-path argument SQLite accepts) from request data, form input, or anything else an attacker might influence. Keep the set of attachable paths fixed and defined entirely by your own application code.

Security Checklist

#Habit
1The .db file (and -wal/-shm in WAL mode) has OS permissions limited to the app's own user, and lives outside any publicly reachable directory
2Every query touching user input uses a parameterized query or prepared statement — never string concatenation
3ATTACH DATABASE paths (and any other file paths passed to SQLite) are fixed by application code, never built from untrusted input
Next: With the security model straight, head to Connecting from Application Code to see exactly how Python, PHP, Node, and Android talk to a SQLite database — including the parameterized-query syntax each one uses.