This error occurs when an authorization callback denies access to a database operation. SQLite's authorization mechanism is typically used to enforce security policies when executing SQL from untrusted sources.
The SQLITE_AUTH error (error code 23) is returned when the authorization callback function registered via sqlite3_set_authorizer() indicates that an SQL statement being prepared is not authorized to execute. This is a security feature that allows applications to control which operations are permitted on a database connection. The authorizer callback is invoked during the compilation phase of SQL statements (sqlite3_prepare() and its variants), not during execution. When the callback returns SQLITE_DENY, the prepare call fails immediately with this error. This error is most commonly seen in applications that use SQLite's authorization feature to implement row-level security, column-level access control, or to restrict dangerous operations like schema changes. It can also occur in sandboxed environments where the operating system restricts file access.
Check if your application or a library you're using has registered an authorization callback. Look for calls to sqlite3_set_authorizer() or equivalent in your language binding:
// C/C++
sqlite3_set_authorizer(db, auth_callback, NULL);# Python
conn.set_authorizer(authorizer_func)// PHP
$db->setAuthorizer($callback);If you find an authorizer and don't need access control, you can remove it by passing NULL:
sqlite3_set_authorizer(db, NULL, NULL);If the authorizer is intentional, debug the callback to understand why it's denying access. Add logging to see which action code is being blocked:
def authorizer(action, arg1, arg2, db_name, trigger_name):
import sqlite3
print(f"Action: {action}, Args: {arg1}, {arg2}")
print(f"Database: {db_name}, Trigger: {trigger_name}")
# Example: Block all DROP operations
if action == sqlite3.SQLITE_DROP_TABLE:
print("Blocking DROP TABLE")
return sqlite3.SQLITE_DENY
return sqlite3.SQLITE_OKCommon action codes include SQLITE_READ, SQLITE_INSERT, SQLITE_UPDATE, SQLITE_DELETE, SQLITE_DROP_TABLE, and SQLITE_CREATE_TABLE. Check the SQLite documentation for the complete list.
Ensure your callback returns the correct value:
- SQLITE_OK (0): Allow the action
- SQLITE_DENY (1): Reject the entire SQL statement with an error
- SQLITE_IGNORE (2): Disallow the specific action but allow the statement to continue (reads return NULL)
Example of implementing column-level access control:
import sqlite3
def column_authorizer(action, arg1, arg2, db_name, trigger_name):
# Allow most operations
if action == sqlite3.SQLITE_READ:
table_name = arg1
column_name = arg2
# Block reading sensitive columns
if column_name == 'password' or column_name == 'secret_key':
return sqlite3.SQLITE_IGNORE # Return NULL instead of data
return sqlite3.SQLITE_OKOn iOS, this error often occurs when the database file has file protection attributes that conflict with background access. When the device is locked, iOS encrypts files and your app may lose access.
Check your database file protection level:
// Swift - Set appropriate file protection when creating database
let dbPath = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("app.db")
// Use .completeUnlessOpen instead of .complete
try FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.completeUnlessOpen],
ofItemAtPath: dbPath.path
)// Objective-C
NSString *dbPath = [self databasePath];
NSDictionary *attributes = @{NSFileProtectionKey: NSFileProtectionCompleteUnlessOpen};
[[NSFileManager defaultManager] setAttributes:attributes ofItemAtPath:dbPath error:&error];Note: File protection must be set before the database is first created. Changing it afterwards won't affect SQLite's internal pages.
If your application is sandboxed (common on macOS), you may need to explicitly request file access:
// Request access using NSOpenPanel for sandboxed apps
let openPanel = NSOpenPanel()
openPanel.canChooseFiles = true
openPanel.begin { response in
if response == .OK, let url = openPanel.url {
// Store bookmark for future access
let bookmarkData = try? url.bookmarkData(
options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil
)
// Save bookmarkData to UserDefaults
}
}For command-line tools, check entitlements in your provisioning profile.
Create a minimal test to verify database access:
import sqlite3
# Test without authorizer
conn = sqlite3.connect('test.db')
cursor = conn.cursor()
cursor.execute('CREATE TABLE IF NOT EXISTS test (id INTEGER)')
cursor.execute('INSERT INTO test VALUES (1)')
print("Without authorizer: Success")
conn.close()
# Test with permissive authorizer
conn = sqlite3.connect('test.db')
conn.set_authorizer(lambda *args: sqlite3.SQLITE_OK)
cursor = conn.cursor()
cursor.execute('SELECT * FROM test')
print("With authorizer: Success")
conn.close()If the first test works but the second fails, the issue is with your authorization callback logic.
Authorization Callback Constraints:
The authorization callback must not modify the database connection that invoked it. Attempting to execute SQL statements within the callback can cause recursive invocation and undefined behavior. Keep callbacks fast and side-effect free.
Action Codes:
SQLite defines over 30 action codes (SQLITE_CREATE_INDEX, SQLITE_CREATE_TABLE, SQLITE_DELETE, SQLITE_DROP_INDEX, SQLITE_INSERT, SQLITE_PRAGMA, SQLITE_READ, SQLITE_SELECT, SQLITE_TRANSACTION, SQLITE_UPDATE, etc.). Each action includes contextual parameters that vary by operation type.
Extended Error Codes:
SQLITE_AUTH_USER (23 | (1<<8)) is an extended error code indicating a user authorization issue in applications that implement user-level access control on top of SQLite's authorization mechanism.
Performance Considerations:
The authorizer callback is invoked multiple times during statement preparation - once for each table, column, and operation involved. For complex queries, this can mean hundreds of callback invocations. Keep authorization logic efficient.
iOS Background Access:
On iOS, databases with NSFileProtectionComplete cannot be accessed when the device is locked. Use NSFileProtectionCompleteUntilFirstUserAuthentication or NSFileProtectionCompleteUnlessOpen for databases that need background access. This must be set before the database file is created.
Security Best Practices:
Use authorization callbacks to implement defense-in-depth security, but don't rely on them as the sole security mechanism. SQLite's authorization feature can be bypassed by attackers with direct file system access. Always use proper authentication, parameterized queries, and file system permissions as primary security controls.
SQLITE_BUSY: The database file is locked
How to fix 'SQLITE_BUSY: The database file is locked' in SQLite
better-sqlite3: This statement has already been finalized
How to fix "better-sqlite3: This statement has already been finalized" in SQLite
SQLITE_CONSTRAINT_CHECK: CHECK constraint failed
CHECK constraint failed in SQLite
SQLITE_READONLY_RECOVERY: WAL mode database cannot be modified
How to fix "SQLITE_READONLY_RECOVERY: WAL mode database cannot be modified" in SQLite
SQLITE_ERROR: SQL logic error
How to fix "SQLITE_ERROR: SQL logic error" in SQLite