☰
SQL注入、XSS、CSRF三大漏洞原理与Spring Boot防御实战
2026/9/26 2:32:54 网站建设 项目流程

1. 为什么这三个漏洞永远是后端安全的必修课

后端安全防护绕不开的三个名字:SQL注入、XSS、CSRF。搞后端开发的多少都听过这三个词,但真要把它们讲透、讲明白怎么防、防到什么程度才算到位,能答上来的人其实不多。我在实际接触过的项目里见过太多这种情况——框架自带了一些安全能力,大家就以为“安全已经搞定了”,直到被安全测试打穿,才回头一个个补窟窿。

这三个漏洞之所以放在一起讲,是因为它们的本质完全不同,但攻击效果又往往叠加在一起:SQL注入直接打数据库,XSS劫持的是浏览器里的用户会话,CSRF则是冒充用户本人去发起请求。如果你能把这三个问题从原理到防御链路全部吃透,后端安全的地基基本就算打牢了。

这篇文章不是教科书式的翻译,我把实践里真正有用的东西整理出来,包括每个漏洞的攻击路径、绕过思路、防御方案,以及在Spring Boot项目里落地的完整代码。适合刚入门的后端开发,也适合做安全加固时缺一份“可抄作业”清单的团队。我会尽量少讲概念、多讲实战,因为安全这东西,光看懂了没用,能挡住攻击才是真的会。

在我开始写代码之前,先把三个漏洞的“信任模型”说清楚,理解了这一层,后面的所有防御手段都是有根的,而不是背配置。

2. 三大漏洞的底层逻辑:攻击者到底利用了哪些信任

2.1 SQL注入:后端把用户输入当成了代码

SQL注入的攻击原理极其简单,但破坏力在三个漏洞里排第一。它利用的是后端对“数据”和“代码”的边界不做区分。用户提交的参数本该是一段普通数据,结果因为拼接SQL语句,被数据库当成了可执行代码。

举一个最简单的例子:登录功能,后端代码写成

String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

如果用户在用户名输入框敲了admin' --,拼接出来的SQL就变成了

SELECT * FROM users WHERE username = 'admin' -- ' AND password = ''

MySQL和很多数据库都把--之后的文本当作注释,所以密码校验被完全绕过了。这就是“万能密码绕过”的底层原理,不是什么魔法,就是一个注释符号的问题。靶场里CTFHub和SQLilabs的前几关基本都在演示这一招,很多新手第一次打穿靶场,体验到“原来还可以这样”的时候,就觉得SQL注入也就这样了。但实际上这只是入门级的第一层,后面还有报错注入、布尔盲注、时间盲注、堆叠注入,每一层的绕过思路和防御要点都不一样。

SQL注入的核心攻击面,永远出在“动态拼接SQL”这个动作上。不管是字符串拼接、还是用了ORM但自己写了自定义SQL、还是排序字段用了传递的列名,只要拼接动作存在,注入风险就存在。

2.2 XSS:浏览器信任了被篡改的前端代码

XSS的全称是跨站脚本攻击,攻击者把恶意脚本注入到网页中,在用户访问时执行。它利用的是“浏览器信任了服务器返回的内容”,或者说服务器把用户提交的不可信内容原样返回给了其他用户。

举一个生活化的类比:这就好比快递驿站代收包裹,驿站公布收件人名单时,有人故意更改了取件码旁边添加了一段“温馨提示”,而驿站管理员不加检查直接原样粘贴出来了。看到这段“温馨提示”的人如果信以为真,点击了里面的链接,就被坑了。

XSS的攻击链条通常是这样的:攻击者在评论区、搜索框、个人资料等位置提交恶意脚本 → 服务器存储或反射这段内容 → 其他用户加载页面时脚本被执行 → 脚本读取用户的Cookie、localStorage、或者直接发起钓鱼请求。热词里提到的反射型XSS、存储型XSS、DOM型XSS,三类我都实测过,后面逐一拆。

对于后端来说,XSS的防御难点在于“输入过滤很难做干净”,因为你不知道哪些字符会在哪个上下文里被HTML解析、被JavaScript执行、还是被URL解析。所以XSS的防御思路必须从“过滤输入”转向“输出编码”。

2.3 CSRF:服务器信任了带Cookie的请求

CSRF(跨站请求伪造)的攻击模型和另外两个完全不同。它不需要读取用户的Cookie,也不需要在用户的浏览器里执行恶意脚本。它利用的是:服务器无法区分“这个请求到底是用户主动发起的,还是攻击者诱导发起的”,因为浏览器会自动携带Cookie。

想象一下这个场景:你登录了银行系统,Cookie留在浏览器里。这时候你在另一个标签页打开一个论坛帖子,帖子里的图片标签src指向http://bank.com/transfer?to=attacker&amount=10000。浏览器加载图片时会自动带上银行系统的Cookie,银行服务器收到请求后一看,Cookie有效、校验通过,就把钱转出去了。

整个攻击过程被攻击者完全控制,但请求携带的身份凭证却是用户的。这就是CSRF的本质:服务器信任了“请求是用户发起的”这个假设,却没有验证“这个意图真的是用户主动产生的”。

后端防御CSRF的核心思路就两条:一是校验不可预测的Token,让攻击者无法构造伪造请求的完整参数;二是校验请求来源(Referer/SameSite),从浏览器层面限制跨站请求携带凭证。

3. XSS类型拆解与Spring Boot项目中的防御落地

3.1 三类XSS的区别和典型场景

反射型、存储型、DOM型,这三类XSS我建议这样记:反射型是“临时弹出来打一次”,存储型是“存进数据库长期打人”,DOM型是“后端完全不知情的纯前端漏洞”。

反射型XSS最经典。攻击者把恶意脚本拼在URL参数里,诱导受害者点击链接,服务器把参数内容直接渲染在响应页面上。CTFHub上的XSS题目大部分都是反射型,就是要你构造一个URL payload,提交到靶场后返回页面执行。这种XSS因为脚本不在数据库中落地,看起来“来去无踪”,但危害仍然不小,尤其是配合钓鱼链接使用时。

存储型XSS的杀伤力最大。脚本写进了评论区、昵称、个人签名这类用户生成内容里,每个访问页面的用户都会中招。热词里提到的Pikachu靶场、DVWA的XSS stored关卡,就是模拟这种场景:先把XSS payload提交进数据库,再让其他用户触发。我在实际项目中遇到过一次情况,用户的昵称字段里被塞了完整的外链脚本,结果后台管理系统每次打开用户列表都弹出垃圾广告弹窗——这就是存储型XSS在企业环境里的真实样子,这个影响范围远比靶场里的演示要广,因为内网系统的Cookie、会话令牌、敏感操作接口全部暴露。

DOM型XSS比较特殊。攻击者的恶意脚本修改的是前端JavaScript的DOM操作逻辑,整个过程服务器不参与,后端拿不到攻击payload,日志里也搜不到异常记录。热词里单独列了“dom型xss”,说明大家开始重视这类纯前端的XSS了。但防御DOM型XSS时,后端能做的事不多,主要依靠前端的编码规范和CSP策略兜底。

3.2 防御XSS的完整链路:输入校验、输出编码、CSP、HttpOnly

很多人一提XSS防御就说“过滤掉script标签”。我见过不少团队就是拿正则替换<script>和onerror这类关键词,但攻击者换个编码方式、换个标签名,规则就失效了。这属于典型的“用战术上的勤奋掩盖战略上的懒惰”。

XSS防御的正确链路按重要性排序如下:

第一,输出编码。服务端渲染页面里,所有插入到HTML上下文的值都必须经过HTML实体编码。比如<转成&lt;,>转成&gt;,"转成&quot;。如果值被插入到JavaScript变量里,要做JavaScript编码;插入到URL属性里,要做URL编码。不同上下文用不同的编码方式,这是XSS防御最核心、最可靠的一层。Spring Boot项目中使用Thymeleaf模板时,默认的th:text就自带HTML转义,但th:utext不转义,要特别注意慎用。

第二,输入校验。对用户提交内容执行严格的类型、长度、字符集白名单校验,业务上合法才继续处理。比如年龄字段必须是数字、手机号必须匹配正则、URL必须以http://或https://开头。输入过滤的目的是减少脏数据进入系统,但绝不能把它当唯一防线,因为总会有绕过的方式。

第三,CSP策略。响应头里声明Content-Security-Policy,限制浏览器只能加载白名单内的脚本来源,从而让注入的未知脚本无法执行。这是我在实际项目中强烈建议要加的兜底手段,即使前面两层没防住,CSP也能大幅缩小XSS的实际危害。

第四,Cookie设置HttpOnly。把会话Cookie标记为HttpOnly,JavaScript就无法通过document.cookie读取,这会直接卡断XSS窃取会话令牌的核心攻击目标。虽然HttpOnly防不住所有XSS攻击(比如钓鱼页面依旧可以伪造表单操作),但它是成本极低、收益极高的一行配置。

3.3 实战:全局Filter统一处理XSS载荷

在很多项目里,XSS防御往往散落在各个Controller里,有的接口过滤了、有的接口忘了过滤,后面对接时就很痛苦。我常用的做法是在Spring Boot项目里写一个全局XSS过滤器(Filter),把“清理请求参数”这个动作统一收口。这也是热词里“springboot项目全局过滤器处理上传pdf文件时xss攻击”指向的场景。

以下是整理的实现思路,我在项目中用的就是这个逻辑:

@Component public class XssFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; // 包装请求,统一清洗所有参数 chain.doFilter(new XssHttpServletRequestWrapper(req), response); } }

核心的XssHttpServletRequestWrapper需要对getParameter、getParameterValues、getHeader这几个方法做清洗,同时要特别注意两个问题:

第一个问题是,清洗不能影响二进制内容的上传。项目里有文件上传功能时,对 multipart 请求里的业务流程参数可以做清洗,但读取文件体本身时要绕过过滤器,否则会把PDF、图片里的二进制内容当字符串处理,轻则乱码,重则文件损坏。我在实现里就对multipart/form-data请求单独判断,文件字段直接透传。

第二个问题是,清洗不能破坏业务字段的原样传输。像富文本编辑器这类字段,本身允许包含html标签用于排版,如果一刀切把所有尖括号都转义掉,业务功能就废了。所以过滤器需要维护一个放行名单,比如content、description这类已知富文本字段可以单独处理。

过滤器的代码如下,清洗时的编码方式要注意放到合适的位置:

public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssHttpServletRequestWrapper(HttpServletRequest request) { super(request); } @Override public String getParameter(String name) { String value = super.getParameter(name); return cleanXss(value); } @Override public String[] getParameterValues(String name) { String[] values = super.getParameterValues(name); if (values == null) { return null; } String[] cleaned = new String[values.length]; for (int i = 0; i < values.length; i++) { cleaned[i] = cleanXss(values[i]); } return cleaned; } private String cleanXss(String value) { if (value == null) { return null; } // 转义关键字符,注意保留业务合法字符 String cleaned = value.replaceAll("<", "&lt;") .replaceAll(">", "&gt;") .replaceAll("\"", "&quot;") .replaceAll("'", "&#x27;"); return cleaned; } }

实操中还要注意一处容易被忽略的坑:如果项目使用了JSON类型的请求体(Content-Type为application/json),Filter默认的清参逻辑是不生效的,因为参数不在request的parameterMap里,而是在InputStream里。针对这类请求,需要在Filter里读取并缓存InputStream,清洗后重新包装,再传给后续的@RequestBody解析流程。这块处理比较繁琐但非常关键,很多团队的XSS过滤器“好像没啥用”就是这个原因——只处理了表单参数,没处理JSON请求体。

4. CSRF攻击链路与几种常见绕过思路

4.1 CSRF的经典利用场景

CSRF的利用场景全部围绕“浏览器自动携带Cookie”这个特性。最常见的攻击载体是图片标签、表单自动提交、以及跨域请求。

图片标签的方式我前面已经举过例子,它是CSRF最精简的攻击形态,无需任何JavaScript。攻击者只要让受害者的浏览器加载一个指向目标站点的URL,就能以受害者的身份触发GET请求。所以对于会改变业务状态的接口,一定不能用GET,这是CSRF防御的第一条红线。

表单自动提交的攻击方式需要用户先访问攻击者的页面,页面的JavaScript创建一个隐形的form表单,字段值预设好,然后调用form.submit()。因为提交是跨站的,浏览器会自动带上目标站点的Cookie,整个动作用户毫无感知。

第三种是JSON跨域请求。现在很多接口改为接收JSON格式,攻击者可以通过Fetch或Ajax构造跨域请求。但浏览器有同源策略,跨域请求会被CORS拦截,所以利用CSRF的前提是目标站点CORS配置过于宽松。我在复试过的一个项目里遇到过Access-Control-Allow-Origin: *加Credentials同时开启的情况,整个CSRF防线形同虚设。

4.2 为什么CSRF Token会被绕过

CSRF Token是同步器模式:服务器生成随机Token,放入用户会话,同时渲染到页面表单里。用户提交请求时带上Token,服务器比对会话中存储的Token和请求携带的Token是否一致。

从原理上看这个方案是无懈可击的,因为攻击者无法读取受害者的会话内容,也就无法在伪造请求里填入正确的Token。但在实践中,我见过很多“看似加了Token却被打穿”的情况,问题往往出在使用不当:

第一,Token校验只做了登录/注册接口,其余业务接口完全没接校验逻辑。第二,Token从Cookie里读取并放入请求参数,攻击者虽然无法读取Token值,但很多框架会把SessionId写入Cookie,而Cookie是会被自动携带的——如果后端把Token直接存在Cookie里再从Cookie校验,那CSRF Token形同虚设。第三,Token不会失效,攻击者只要搞到一个有效Token就可以反复利用。第四,上线前开发环境关闭了CSRF校验,上线后忘了打开。

这里也要补充一个点:有些框架的CSRF Token是按会话生成、所有接口通用的。一旦某个最普通的接口存在XSS,攻击者通过XSS就能拿到页面里的Token,然后配合CSRF完成组合拳攻击。所以CSRF防护不能只看它自身,还要看系统里有没有其他入口泄露Token。

4.3 CSRF防御措施:SameSite、双重提交Cookie、自定义Header

在防御CSRF时,我习惯按“浏览器层 → 应用层 → 业务层”的顺序逐层加固。

浏览器层,最有效的方案是给关键Cookie设置SameSite属性。SameSite=Strict表示完全禁止第三方请求携带Cookie;SameSite=Lax则允许顶级导航GET请求带Cookie,但阻止iframe、图片、表单POST携带。实际项目中考虑用户体验,Lax比较常见。Chrome和Firefox等主流浏览器默认把这个属性的默认值改成了Lax,这让纯浏览器层面的CSRF风险降低了不少,但Safari等浏览器对SameSite的支持和默认行为并不完全一致,所以应用层Token校验仍然不能省略。

应用层,Spring Security自带CSRF保护,配置启用后,所有PATCH、POST、PUT、DELETE请求都会校验_csrf参数或请求头。前后端分离的项目,前端从Cookie或响应头读取Token后放在每次请求的自定义请求头X-CSRF-TOKEN里,后端过滤器比对。自定义请求头本身就是一种CSRF防护手段,因为跨站攻击无法自定义请求头,而跨域请求若要带自定义头必须先通过CORS预检,CORS配置正确时跨站请求会被拦截。

如果用的是Spring Security的配置方式,可以这样开启:

http.csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) );

withHttpOnlyFalse()是为了让前端能够读取到Cookie里的Token,但这意味着XSS能直接读取Token,所以单靠这一条是不够的,需要配合XSS防护一起使用。

业务层,还有两个值得说的兜底方案:一是关键敏感操作(改密、转账、删除)做二次校验,比如输入短信验证码、图形验证码,从业务层面确保操作是本人在场主动发起的;二是校验请求的Origin和Referer,虽然Referer可以被部分环境隐藏或伪造,但作为一种辅助判断手段,成本很低,至少能把一些跨站攻击挡掉。

5. SQL注入利用手法与参数化查询的本质

5.1 联合查询、报错注入、布尔盲注、时间盲注的排查思路

SQL注入的利用手法排序大致是:联合查询注入、报错注入、布尔盲注、时间盲注、堆叠注入。我在给团队做培训时经常这样比喻:联合查询是“直接打开数据库的窗户看数据”;报错注入是“让数据库把错误信息直接念给你听”;布尔盲注是“问数据库一百个是非题,拼凑出答案”;时间盲注是“让数据库用延迟多少秒来回答是非题”。

每种类型的排查路径各有侧重。联合查询注入的本质是UNION SELECT把查询结果合并出来,前提是目标查询的列数和后端渲染逻辑可控。报错注入靠的是数据库函数报错时把参数内容带出来,比如MySQL的updatexml()和extractvalue(),报错信息里直接带出查询结果。布尔盲注和时间盲注是完全“盲打”的状态,服务器不回显任何查询结果,攻击者只能通过页面位置是否正常和响应时间的差异来逐字符推断数据,速度慢但危害一点不小。

对于后端开发来说,了解这些利用手法的意义不在于“学会攻击”,而在于明白一个道理:只要字段值被拼进了SQL,无论用哪种手法,攻击者都可以组合出适合当前场景的利用方案。数据最终是流向页面回显、还是流入错误日志,决定了利用手法包装成什么样,但漏洞根源始终是拼接。

5.2 参数化查询为什么能防SQL注入

防SQL注入的正确姿势有且只有一个核心:参数化查询。所谓参数化,就是SQL语句的结构里,值的位置用占位符?表示,由驱动把参数单独传给数据库。数据库收到的是“结构和参数分离”两件东西,参数永远只被当作数据来解析,永远不可能变成SQL结构的一部分。

在Java里,JDBC的方式是PreparedStatement:

String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs = ps.executeQuery();

在MyBatis中,对应的就是#{}占位符:

<select id="findByUsername" resultType="User"> SELECT * FROM users WHERE username = #{username} AND password = #{password} </select>

#{username}会被MyBatis编译成PreparedStatement参数占位符,安全可靠。而${username}是直接把值拼进SQL原文,等同于之前的字符串拼接,存在注入风险。MyBatis里ORDER BY ${column}这种场景我发现不少开发者想偷懒直接拼接,虽然排序字段确实无法用预编译占位符处理,但这属于白名单校验的范围,不是不能绕过去做防护——在代码里先校验排序字段只包含指定的几个列名即可。

有一个在真实项目里反复出现的坑是:很多人以为用了MyBatis就万事大吉,然后在动态SQL里写了${}直接拼接。MyBatis帮你在99%的流程上做了参数化,只有那1%的拼接点,恰好就是攻击者要盯住的入口。

5.3 WAF绕过思路带来的启发:防注入必须做在应用层

热词里频繁出现的“sql注入绕过”“sql注入万能密码绕过”,背后的绕法五花八门:注释符绕过、大小写混合、等价函数替换、内联注释、URL编码、百分号拼接、看不见的空格符、以及数据库函数与运算符的魔幻结合。WAF的存在确实挡掉了大量脚本小子的扫描流量,但WAF本质上是基于规则的模式匹配,始终存在绕过空间。

我记得有一次打靶场的经历挺典型:过滤了关键字select,那就试SeLeCt;过滤了information_schema,那就用内联注释/*!50000information_schema*/;空格被过滤了,那就换%0a换行符代替。只要人工测试的耐心够,规则类拦截几乎都有办法找到破绽。

这个现象带来的启发很重要:安全防护不能押注在单一层。WAF可以当作外层的粗筛,但应用层必须有自己的底线——参数化查询、最小权限原则、错误信息脱敏。数据库连接账号只授予所需的最小权限,应用不用的高权限操作坚决不收;数据库报错信息永远不要原样回传给前端,避免注入时被攻击者当作信息回收站。

另外,CTF靶场里那些利用手法在真实企业系统中很难完全重现,因为线上环境往往有多层防御。但反过来想,那些被攻破的生产事故,常常不是在复杂绕法上翻车的,而是在最简单的入口上翻车的——比如一个可以直接拼接的SQL点、一个没做任何转义的搜索框。安全审计时,建议把目光先放在这些高风险入口上,不要一上来就钻研复杂的编码绕过。

6. 综合防护实践:在Spring Boot项目里做一套纵深防御

6.1 纵深防御的思想:不要赌任何单点防护

我在实际项目里的安全加固原则一直是一句话:“不要赌任何单点防护。”意思是每一层防御都有可能在特定条件下被绕过,但多层叠加之后,攻击者的综合利用成本会指数级上升。

前端输入校验是第一层,能拦截大多数正常用户的误操作,但防不住刻意构造的请求。XSS过滤器是第二层,对所有请求参数统一做清洗,挡住了绝大多数反射型XSS。CSP和HttpOnly是第三层,即使XSS载荷真的到达了浏览器,也没有脚本执行空间,即使脚本执行了,也读不到Cookie。参数化查询和MyBatis#{}是第四层,保证应用代码层面不会出现注入拼接。CSRF Token和SameSite是第五层,确保跨站请求携带的凭证根本无法通过校验。

针对上传场景还要单拎出来说,因为上传接口历来是高危面。如果一个项目允许上传PDF,PDF内容里很可能包含JavaScript逻辑,浏览器在预览PDF时如果执行了内嵌脚本,就会出现热词里说的“上传PDF文件时XSS攻击”场景。Spring Boot的全局Filter只能处理请求参数,管不到PDF文件内部的恶意内容。稳妥的做法是两层配合:一是限制上传文件的类型和大小,不只以扩展名判断,还要检查文件的实际内容签名;二是响应头每次都带上Content-Disposition: attachment,让浏览器以附件形式下载而不是内联预览,并且给PDF文件单独设置Content-Security-Policy响应头。

6.2 一套可以直接抄的后端安全自检清单

每次项目上线前,我都会按这张清单过一遍,既当自查工具,也当新人培训材料。它覆盖了三大漏洞的主要防护点:

  • 所有SQL语句是否统一走PreparedStatement/MyBatis#{},代码库里是否还有String.format()拼SQL的情况
  • 数据库账号权限是否最小化,是否用了高权限账号连接业务库
  • 所有渲染到页面上的动态值是否都做了HTML编码,th:utext是否已全部排除
  • 富文本字段是否单独校验标签白名单(只允许p、strong、em、ul、li等基础标签),是否允许a标签并校验href协议
  • 会话Cookie是否设置了HttpOnly和Secure,生产环境是否全部开启了HTTPS
  • 是否配置了CSP响应头,script-src是否按'self'白名单方式收紧
  • 页面表单/请求是否带CSRF Token,Spring Security的CSRF保护是否在线上环境保持开启
  • 文件上传接口是否校验了文件内容类型,是否禁止了脚本类文件,下载接口是否强制附件下载
  • 错误响应是否统一为脱敏JSON,不泄露SQL结构、堆栈信息等运行时细节

这张清单我也在线上小规模团队里推行过,实测效果很好的地方不在于“每条都做到”,而在于“每一次iteration都要重新过一遍”。因为安全漏洞往往是在新增一个模块、换一个接口方式时被不小心带出来的,不是上个版本做完了就一劳永逸。

6.3 关于FastJson、文件上传与JSON请求体的XSS清洗经验

JSON请求体的XSS清洗,我前面简单提了一下,这里专门展开说,因为它是Spring Boot项目中XSS过滤器最容易失效的隐藏点。处理JSON字符串时,不能整体replace特殊字符,那会把JSON结构本身弄坏。正确的方式是按key逐个清洗字段值,再重新序列化成一个JSON字符串,回填到请求流里。如果项目里用了FastJson或Jackson,对每个String类型字段统一做一次XSS清洗即可。

文件上传的清洗则要谨慎得多。文件内容本身的合法性校验应交给文件类型识别,而不是XSS过滤器。可以依赖阿帕奇Tika或文件头魔数判断真实类型。上传PDF时有一个实操技巧:PDF文件内部可以含JavaScript动作,市面上成熟的方案是扫描PDF里是否出现/JavaScript和/OpenAction标记,出现就直接拒绝。这个方案的拦截效果我实测过几次,对绝大多数携带恶意脚本的PDF都有效,而且实现成本不高,可以做成一个单独的UploadValidator。

整个清洗链路中最容易被遗忘的,是Async请求和WebSocket消息。这些通道不走传统Filter链路,如果你只在doFilter里做了防御,异步通道会变成一个“裸奔”的入口。处理方式是为异步请求单独注册专门的拦截器或接口层的校验逻辑,保证所有业务入口都经过同一套安全校验。

6.4 安全头的整体配置与一处容易被忽略的业务逻辑风险

关于安全响应头,Spring Security中可以统一配置,聚合效果比零散放header强得多:

http.headers(headers -> headers .contentSecurityPolicy("script-src 'self'; object-src 'none'") .and() .httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).maxAgeInSeconds(31536000)) .and() .contentTypeOptions() );

X-Content-Type-Options: nosniff禁止浏览器猜测响应内容的MIME类型,Strict-Transport-Security(HSTS)强制HTTPS连接,CSP限制脚本来源,这三项组合在一起就能极大地压缩中间人攻击和XSS执行空间。

不过安全不是配置堆出来的。在做CSRF/XSS防护时,我遇到过团队花大力气做好了技术防护,却在业务逻辑上翻车的情况:比如改手机号接口虽然校验了CSRF Token,但用户输入新手机号后,系统只是简单回显了“您的新手机号是:xxx”,没有做任何输出编码,结果这个回显点又成为了存储型XSS的温床。所以安全加固必须对代码的输入、输出两条链路都走一遍,不能只在某一个“听起来像安全”的地方死磕。

实操时还有一个小经验:上线前的安全自测不要只用自己构造的请求。把DVWA、Pikachu、CTFHub这几个靶场的题目当练习册,从低级到高级挨个打通关,触类旁通后回到自己的项目里做漏洞检查,能发现很多“教科书上没有写但攻击者就在用”的薄弱环节。但要注意,靶场是训练环境,练手时务必在自己本地搭建,不要对任何线上系统做未授权测试,这条法律红线踩不得。

还有一个很多团队容易忽略的地方:生产日志里不要记录完整的请求参数和Cookie。一旦日志被拖走,等于把用户会话凭证送给了攻击者。我经手过的项目里,日志统一做了脱敏处理,手机号、身份证号、Token、Cookie关键字段一律打码或者用加密方式记录,只保留必要的排查信息。这些细节单独看都不起眼,但组合起来,才是“后端安全防护”四个字真正的分量。

7. 常见问题与排查技巧实录

实操中我遇到了不少具体问题,整理成一份速查表,方便你对照排查:

问题现象可能原因排查路径与解决方案
XSS过滤器把正常富文本内容弄乱了Filter对所有字段统一做了转义,未放行富文本字段检查XssHttpServletRequestWrapper,维护富文本字段白名单,单独处理,不做尖括号转义
JSON请求体注入XSS payload依然生效Filter只处理了表单参数,未读取JSON request body在Filter中读取并缓存InputStream,按JSON字段逐个清洗,重新包装请求流
前端拿不到CSRF TokenCookie被设为HttpOnly或者前端从非标准位置读取使用withHttpOnlyFalse()配置,或者改为接口返回Token给前端存内存变量
MyBatis报SQL注入漏洞动态SQL里用了${}拼接全部改为#{},必要的排序字段改为代码内白名单校验
上传PDF触发XSS告警PDF文件内部包含JavaScript动作上传时扫描/JavaScript、/OpenAction标记,下载时强制Content-Disposition: attachment
CSRF Token校验偶尔失效前端异步请求没有携带Token请求头统一在请求拦截器中加X-CSRF-TOKEN头,或者用自定义Header方案
时间盲注排查时响应极慢数据库并发压力或者请求被WAF限速检查是否有人正在跑盲注脚本,看慢查询日志定位相似的异常查询语句
WAF报告SQL注入但代码已参数化参数里携带了类似SQL的关键字触发规则误报检查WAF规则类型,对合法业务参数加白名单,同时确认应用层确认参数化不需要额外担心

排查的思路永远是先确认漏洞入口的真实性,再顺着入口往上游看数据拼接链路,最后结合防御层逐层验证。不要一看到安全报告就急着改代码,先把“这个漏洞到底能不能被实际利用”搞清楚,有的放矢。

关于日志里看到的可疑SQL,我再补一句:如果你在日志中发现某条查询的WHERE条件里莫名多了一段OR 1=1或者AND SLEEP(5)之类的语句,基本可以确认有人正在对你发起SQL注入探测。正确做法是先拦IP、查Nginx/WAF访问日志中的请求来源和参数,再检查这条SQL对应的代码点是否确实采用了参数化查询。如果确认代码没问题,多半只是扫描器在广撒网,不必过度紧张;但每一条探测记录都应该被记录和溯源。

最后分享一个我个人的经验:做后端安全防护,不要把希望寄托在“用某一个框架/某一个工具”上。框架能提供的都是默认能力,真正决定系统安全级别的,是你对攻击链路的理解深度和在代码里付出的每一行校验。把SQL注入、XSS、CSRF这三块吃透之后,再去学越权、文件上传、反序列化等高级主题会顺畅很多,因为安全的核心思维方式是共通的:永远怀疑外部输入,永远校验请求意图,永远给数据标注边界。每次迭代上线前,照着上面的自检清单过一遍,长期坚持下来,系统的安全水位一定能稳定提升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询