
AI让漏洞挖掘成本骤降,开发者需在首个提交就重视安全
来源标题:Adding proper security to your vibecoded Linux GUI apps
AI智能体大幅降低了漏洞利用成本,过去靠隐蔽性保护的小型系统已不再安全。开发者应从第一次提交代码起就采取安全优先姿态,即便是hello world程序也要加入适当的安全措施。作者以其个人构建的桌面银行与投资追踪应用为例,说明了实践中如何落实安全防护。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
过去几周,我们目睹了大量漏洞在人工智能的助力下被接连不断地曝光。我此前在《为什么我们不编写严格的数学形式化证明来获得无 bug 的代码》一文中已经谈过这个话题——当时整个项目开始被自动化审计逐一扫描。
我认为这是一件大好事。这些缺陷绝大多数都源自几十年前就已知的风险,它们之所以还留在生产环境里,仅仅是因为开发者想当然地认为没人会去翻看代码库的那个角落。有了 AI agent,利用简单漏洞的成本变得极低。那种曾经看似能保护小型系统的“隐晦式安全”,一夜之间就蒸发了。
你必须从第一次提交代码起就重视网络安全。锻炼这种能力的唯一方法,就是始终保持安全优先的姿态,哪怕是写 hello world 也要加上正规的安全措施。
为了展示这在实践中如何运作,我以自己开发的一个桌面银行与投资追踪应用为例。出于显而易见的原因,我把源代码放在家里的私有 Gitea 服务器上,永远不会开源。不过,我可以分享一些我为它设计的安全措施。请把这篇演示当作解决问题的一种方式(在众多可行方案之中),旨在给你启发,作为进一步学习的起点。
这个应用是一个用 Rust 编写的本地 GUI,界面基于 QtQuick/QML(通过 Quickshell 实现),并使用本地 SQLite 数据库。它把账户余额、股票投资组合、固定收益债券到期日、备兑看涨期权计算以及现金流跑道预测整合到同一个界面里。请注意,上下截图中显示的所有数字、余额、银行名称(Institution Alpha、Institution Beta)和股票代码都是 100% 的假数据,由应用内置的 --demo 模式生成。
为什么这个应用要自动化一个浏览器,而不是直接调用干净的 REST API?因为大多数零售和私人网上银行门户用起来极其繁琐,而且完全不向个人客户提供 API 访问。
手动做这件事需要登录每家银行,按日期范围查询对账单,下载 Excel 表格或交易确认 PDF,再手动把数字复制粘贴到别处。在我目前的工作流里,应用会打开一个受控的浏览器窗口,我手动登录并在银行的真实页面上完成 2FA,点击“接管”按钮,自动化流程随即严格接管,只负责抓取对账单、写入我的数据库,然后关闭会话。
在第一个实验版本里(就是你为了看到屏幕上的图表能跑起来、在一阵 vibecoding 的冲动中拼凑出来的那个版本),我把所有能省的环节都省了:
我平时对安全非常偏执,就连我也因为知道这些数据全都以明文形式躺在我的 SSD 上而感到焦虑。即便是在我自己的私人电脑上、锁在我办公室里没人能物理接触到,任何恶意 npm 包、流氓浏览器扩展、Python 脚本,或是在我的 UID 下运行的被入侵的二进制文件,都能完全访问我的财务生活。
如果单个以你的用户账户运行的被入侵进程就能 cat 你的数据库,或者附加到已登录你银行的浏览器的调试端口,那你的架构就有严重缺陷。
我决定停下一切,像对待关键生产系统一样重写这个项目的安全基础。让我们一步步走过每一层,难度控制在任何初级开发者都能跟上并应用到自己的项目中的水平。
显而易见的第一步,是把 SQLite 数据库从明文里拿出来。在 SQLite 生态中,透明页级加密的黄金标准是 SQLCipher(在 Rust 中通过 rusqlite crate 的 bundled-sqlcipher-vendored-openssl feature 使用)。SQLCipher 不是零散地加密单个列,而是逐页加密整个数据库文件(使用 AES-256-CBC 加 HMAC-SHA512 认证)。表名、索引,甚至标准文件头(SQLite format 3\0)在磁盘上都与随机噪声完全无法区分。
为了保证任何连接都不会意外地以弱默认值打开数据库,每个连接都要经过一个初始化器,它会拒绝明文文件头,并在执行任何查询之前强制应用加密配置:
const RAW_SQLITE_MAGIC: &[u8; 16] = b"SQLite format 3\0"; const CIPHER_PAGE_SIZE: u32 = 4096; const CIPHER_KDF_ITER: u32 = 256_000; fn apply_cipher_pragmas(conn: &Connection, key: &DatabaseKey) -> Result<()> { let key_pragma = format!("PRAGMA key = "{}";", key.sqlcipher_raw_key_literal()); conn.execute_batch(&key_pragma) .map_err(|_| VaultError::InvalidKeyOrCorruptDatabase)?; conn.execute_batch(&format!( PRAGMA cipher_compatibility = 4; PRAGMA cipher_page_size = {CIPHER_PAGE_SIZE}; PRAGMA kdf_iter = {CIPHER_KDF_ITER}; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512; PRAGMA cipher_hmac_algorithm = HMAC_SHA512; PRAGMA cipher_plaintext_header_size = 0; PRAGMA cipher_memory_security = ON; PRAGMA foreign_keys = ON; PRAGMA secure_delete = ON; PRAGMA temp_store = MEMORY; PRAGMA trusted_schema = OFF; ))?; let : i64 = conn .query_row("SELECT count(*) FROM sqlite_schema;", [], |row| row.get(0)) .map_err(|| VaultError::InvalidKeyOrCorruptDatabase)?; Ok(()) }
如果你正在学习这套机制如何运作,下面是上面每个 PRAGMA 的作用:
当你已经在一个明文 SQLite 文件里积累了数月的财务历史时,跑一个草率的迁移脚本无异于自找数据损坏。我构建了一条带门控的迁移流水线,由确定性的数学完整性校验(逻辑指纹)作为支撑:
pub fn compute_logical_fingerprint(conn: &Connection) -> Result<String> { let mut hasher = Sha256::new(); update_tagged(&mut hasher, "fingerprint-version", "1"); let objects = load_schema_objects(conn)?; update_tagged(&mut hasher, "schema-object-count", &objects.len().to_string()); for obj in &objects { update_tagged(&mut hasher, "schema-object-kind", &obj.kind);
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。