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 / PostgreSQL | SQLite | |
|---|---|---|
| Who can connect | Controlled by CREATE USER + authentication | Anyone who can open the file |
| What they can do | Controlled by GRANT privileges per user | Whatever the file's OS permissions allow — all or nothing |
| Where enforcement happens | Inside the database server | Nowhere — it's delegated entirely to the operating system |
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
.dbfile (and, in WAL mode, its-waland-shmcompanions) 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 |
|---|---|
| 1 | The .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 |
| 2 | Every query touching user input uses a parameterized query or prepared statement — never string concatenation |
| 3 | ATTACH DATABASE paths (and any other file paths passed to SQLite) are fixed by application code, never built from untrusted input |