Electron+SQLite存在SQL注入风险,因渲染进程传参、主进程拼接SQL或用户可控路径等场景均可能触发;防御关键在于严格使用参数化查询(如?占位符)、表名列名白名单校验、IPC通信输入过滤及禁用敏感SQL操作。

Electron应用中使用SQLite时,SQL注入风险真实存在,但和Web后端不同:它不来自HTTP请求,而是源于渲染进程传入的未校验参数、主进程拼接的动态SQL,或用户可控的文件路径。防御的关键不是“能不能用SQLite”,而是“怎么用才不把漏洞塞进本地数据库”。
为什么 Electron + SQLite 也会被SQL注入?
很多人误以为本地数据库没网络就安全,其实不然。攻击面主要在三处:
- 渲染进程通过
ipcRenderer.send()发送查询条件(比如搜索关键词、排序字段名),主进程若直接拼进SELECT * FROM users WHERE name = '+ name +',单引号一插就完事 - 用户可修改的配置项(如自定义导出路径、表名前缀)被用于构造
CREATE TABLE或ATTACH DATABASE语句 - 从
localStorage、JSON配置文件或拖入的CSV中读取字段名/值,未经白名单过滤就进SQL
SQLite本身不区分“客户端”或“服务端”,只要SQL字符串由不可信输入参与拼接,就可能触发注入——哪怕只是删掉自己本地的一个表。
必须用 sqlite3 的 run() / get() / all() 参数化接口
Node.js 的 sqlite3 模块原生支持参数化查询,这是唯一推荐的方式。它底层调用 SQLite 的 sqlite3_bind_* 系统,确保参数永远作为数据而非语法解析。
正确写法:
db.get('SELECT * FROM notes WHERE id = ? AND status != ?', [noteId, 'deleted'], (err, row) => { /* ... */ });错误写法(绝对禁止):
db.get(`SELECT * FROM notes WHERE id = ${noteId}`, callback); // 危险!注意点:
- 问号占位符
?适用于所有值;命名参数:name或$name也支持,但需保持风格统一 - 表名、列名、ORDER BY 字段不能用参数化——它们属于SQL结构,必须走白名单校验或硬编码
- 如果真要动态列名(比如用户选择排序字段),先查
PRAGMA table_info(notes)获取合法字段列表,再用includes()判断是否在其中
主进程与渲染进程通信时,别让IPC变成注入通道
IPC 是 Electron 中最常被滥用的注入入口。常见错误是主进程收到 search:query 事件后,不做清洗就代入查询:
ipcMain.on('search:query', (event, keyword) => {
db.all(`SELECT * FROM items WHERE title LIKE '%${keyword}%'`, ...); // ❌
});应改为:
ipcMain.on('search:query', (event, keyword) => {
// 强制截断长度、过滤控制字符、拒绝空格外的空白符
const safeKeyword = keyword?.toString().slice(0, 100).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
db.all('SELECT * FROM items WHERE title LIKE ?', [`%${safeKeyword}%`], ...); // ✅
});额外建议:
- 在
webPreferences中始终启用contextIsolation: true和nodeIntegration: false,避免渲染进程直接 require('sqlite3') - IPC 事件名不要暴露业务语义(比如不用
db:delete-user),改用抽象名如data:remove,并在主进程中做完整权限判定 - 对敏感操作(DROP、ATTACH、PRAGMA writable_schema)一律禁止,或仅允许预设的固定语句
绕不开动态表名?那就用白名单 + 正则双保险
有些场景确实需要运行时指定表名,比如多租户笔记应用按年份分表(notes_2025, notes_2026)。这时不能靠参数化,但可以这样控制:
const validTablePattern = /^notes_\d{4}$/;
if (!validTablePattern.test(tableName)) {
throw new Error('Invalid table name');
}
db.get(`SELECT count(*) FROM ${tableName} WHERE id = ?`, [id], ...);关键点:
- 正则必须严格锚定(
^和$),防止notes_2025--或notes_2025%00绕过 - 白名单优先于正则:如果只有 3 个固定表,直接用
['notes_2024', 'notes_2025', 'notes_2026'].includes(tableName) - 永远不要把用户输入的任意字符串当表名拼进SQL,哪怕加了引号也不行(SQLite 支持
"table name",但引号本身可被注入利用)
真正容易被忽略的,是那些看似“只读”的查询——比如导出功能里让用户选字段再生成 SELECT 语句。只要字段名没进白名单,status AS "a' UNION SELECT password FROM users--" 就能跑通。防御SQL注入不是加一层过滤,而是从数据流向的第一步就切断拼接链。

















