Java EE 企业应用 27:CSRF、XSS、SQL 注入与 CORS 各挡在哪里
同一张审批单有四条不同的攻击路径
审批员在浏览器里已经登录。一封邮件诱导访问第三方页面,使浏览器携站点 Cookie 向采购系统提交审批,这是 CSRF;申请备注里的 <script> 被审批页面当 HTML 输出是 XSS;客户端在搜索框输入 x' OR 1=1 --,SQL 字符串拼接引起越权查询是 SQL 注入;开放 Access-Control-Allow-Origin 只是允许特定浏览器脚本读取跨源响应,不授予审批权限,也不会自动阻止跨站请求。四者影响面不同,不能用“一律转义输入”概括。
现有 LabApprovalPage 与 /servlet/lab-session 是隔离教学入口,不是正式登录审批页;LabRequestsResource 无可信认证。第 24–26 篇拟议的身份和对象限制接入以前,不能把这些页面或 REST 端点称为受保护。以下代码/HTTP 协议是计划中的受保护入口,未接入工程;正常与攻击演练均 NOT_RUN。
从来源到提交动作
对基于 Cookie 的浏览器会话,服务器在受信 GET 表单时生成高熵、每个会话绑定的不可预测 CSRF 值,经 HTML 字段或经同源脚本请求头回送;修改状态的 POST 校验会话与令牌,缺失或不匹配时拒绝,且不执行用例。认证成功时更新会话 ID,注销时清除相关状态;令牌不能写进 URL、日志或跨域可读的响应。SameSite Cookie 限制和 Origin/Referer 检查可作附加防线,不代替服务器令牌校验;禁止用 GET 执行审批。无 Cookie 的明确 Bearer API 与 Cookie 会话的威胁模型不同,但 XSS 仍可能借已登录页面发请求;不能简单说 Bearer API 不需要其他防护。
以下 Servlet 代码是局部示意:前提是已经认证的受管端点,session.getAttribute("csrf") 是在表单 GET 时由服务器安全随机源生成并保存在同一会话的值;对实际系统应由认证边界内的统一 Filter 覆盖所有改变状态的路由,并对会话轮换重新发放令牌。
1 | |
目标代码入口是 POST /api/requests/{id}/approve:浏览器会话保护层在解析请求后、调用 ApprovalService.approve 前调用 CsrfGuard.allows(request);返回 false 时立即结束请求,返回 403,不得调用 approve。该辅助类可读但没有接线,也没有生成或轮换令牌的实现;getParameter 还可能读取 URL 查询参数,正式实现应禁止把 CSRF 值放入 URL 并对表单/JSON 入口分别选定接收位置。仅复制它不会保护任何 URL。缺令牌时数据库状态与审计中都不应出现成功审批。正式接入时需防止认证后旧会话继续持有令牌;还要验证代理后的 HTTPS/Cookie 配置。
输出、SQL、跨源分别守边界
HTML 正文中的供应商名与备注用上下文正确的 HTML 编码;放进属性、URL 或 JavaScript 字符串时需相应上下文编码或避免拼字符串。REST JSON 响应由 JSON 序列化处理字符串边界,但把 JSON 内容赋给 innerHTML 仍会造成 DOM XSS,应使用 textContent。含上传文件名的响应也不能当可执行 HTML 返回。既不推荐全库提前 HTML 转义,也不能把用户可见“已过滤”当安全依据。
JDBC 值用 PreparedStatement 参数绑定,如 SELECT ... FROM purchase_request WHERE tenant_id=? AND id=?,不能把请求体拼进 SQL;动态排序列或表名不能用 ? 绑定,应对允许字段做固定白名单映射,列表查询仍需租户条件和授权。X-Lab-Tenant 即便作为绑定参数也依旧是不可信租户;防注入不等于防越权。
CORS 如确有跨站前端需求,只为固定的 HTTPS 源配置 Access-Control-Allow-Origin;凭据模式不得返回通配 *,明确允许的方法和头,并在服务器端继续认证、校验 CSRF(如 Cookie 方案)与对象权限。预检 OPTIONS 成功不是 POST 审批成功;非浏览器 curl 不受 CORS 限制。错误来源的跨源脚本不应读到凭据化响应,但 Origin 可伪造,服务端不可用它代替身份认证。
可复验的攻防条件
待正式入口具备身份与会话后:正常路径由已登录审批员携有效会话+CSRF 值提交自己的合法待批单,检查状态和版本;失败路径同一 Cookie 缺少或篡改令牌,应返回 403 且状态不变。向合成供应商名注入 <img src=x onerror=alert(1)> 并检查浏览器 DOM 是文本而非执行;查询参数输入 x' OR 1=1 --,确认 SQL 只按该字面值查询且另一租户零结果;从未授权 Origin 发带 Cookie 的预检/实际请求,核对 CORS 响应与服务端拒绝。浏览器测试需要真实 HTTPS、会话、证据截图或 DOM/网络记录,服务端则核对最终数据库行。
**现有工程未提供上述受保护正式审批/搜索端点及 CSRF Filter:以上正常、缺令牌、XSS、注入和 CORS 实验全部 NOT_RUN。**即使现有 scenarios/09-lab-procurement.sh 在本机通过,也不能证明这些攻击被阻断;不应拿伪造租户头的 curl 当认证测试。
CSRF 校验要覆盖整条状态改变路径
CSRF 利用的是浏览器在访问站点时自动附带现有 Cookie,第三方页面不需要知道会话密钥,也能尝试提交表单或诱导导航。令牌校验若只挂在页面表单而绕过 JSON REST 路由,同一个审批用例仍可能从未保护的路径进入。部署时应列出所有能创建、提交、批准、拒绝和下单的 URL 与 HTTP 方法,确认每条基于 Cookie 的改变状态请求都在业务方法之前经过校验;登出或其他状态改变同样应考察。GET 必须无副作用,不能用“只允许 POST”代替令牌,因为跨站表单也能发送 POST。
现有 CsrfGuard 只从 request.getParameter("csrf") 读取令牌;这适用于表单参数或查询参数的 Servlet 解析,但不等于能从 application/json 请求体自动取得令牌。若计划中的审批 REST 用 JSON 体,设计应约定由同源脚本在专用请求头发送令牌,过滤器从头字段读取,再与 Session 中的值比较;不能在 Filter 中试图抢先读取请求体后让 JAX-RS 再解析一次而不处理输入流。切换传输方式前还要明确服务器只从预期位置取值、拒绝缺失和重复值。令牌生成应使用安全随机源,登录会话更新时重设,缓存/日志不能泄漏;客户端提交旧令牌时要观察拒绝与申请状态保持不变。
Session 与令牌都是会话安全的一部分,但承担不同责任。认证后未轮换的旧 Session ID 可能被攻击者预先固定;轮换会话 ID 防这种会话固定风险。CSRF 令牌阻止不知道会话内秘密的外站请求,它不能防同源 XSS:恶意脚本若已在本站运行,就可能读页面里的令牌并发起合法格式请求。因此对含供应商备注的页面做上下文正确的输出编码仍是必需工作。SameSite Cookie 可降低某些浏览器的跨站携带行为,但浏览器版本、顶级导航和同站子域场景会影响结果,不能单凭某个 Cookie 属性宣布 POST 无法被滥用。
XSS 应在输出上下文解决,不在数据库里预先改写
同一段供应商备注放入 HTML 正文、双引号属性、JavaScript 字符串或 URL 参数,需要不同的安全表达。HTML 正文编码 < 和 & 并不足以安全地拼成 JavaScript 源码;最少权限做法是尽量不把用户数据嵌进脚本字面量,而通过服务端 JSON 序列化并在浏览器使用 textContent 构造文本节点。展示报价文件名时还要防止文件名进入 HTML 属性或响应头时成为语法的一部分。若把“安全编码后的备注”存回原始数据库字段,未来要导出 CSV、生成 JSON 或发通知时会拿到已经丢失语义的展示文本;应保留业务原值,在每个输出边界编码。
例如合成输入 <img src=x onerror=alert(1)>,服务端渲染页面的合格观察不只是“响应里出现了 <”:要在冻结浏览器中确认 DOM 中它是文本节点、没有创建可执行的 img,且列表、详情、错误页面和审批页的所有显示路径一致。如果提供富文本,则必须按允许的 HTML 元素/属性白名单做专业净化,并对每个输出位置验证,不能仅使用字符串替换。内容安全策略可以减轻部分脚本执行后果,但不能替代正确编码,也不能授权用户可调用的业务 API。
参数化的是值,SQL 结构仍由服务端决定
PreparedStatement 把用户输入当值,而 ORDER BY ? 不能可靠地让用户选择列名;试图拼接原始 sort 字符串就是重新开放 SQL 语法入口。只允许确定的服务端列映射,选择非法字段时返回输入错误,不默认把恶意列名继续交给数据库。以下可编译的独立 Java 辅助类示意给出映射规则,不在当前累计工程中:
1 | |
查询主体依旧需要 WHERE request.tenant_id=? AND request.status=?,两个参数从受信租户和受约束枚举绑定;排序片段只能取上述两个服务端固定字符串。绑定查询值防止注入,但即使把 X-Lab-Tenant 绑定得完全正确,也还是按攻击者自报的租户查数据。安全测试应把注入、租户越权和角色权限当三张不同的断言表,分别保存 SQL 结果与响应,不因某个查询没有 SQL 错误就认为所有问题解决。
CORS 响应为什么不能写成服务器鉴权结果
浏览器的预检会问服务器是否允许从某个源用指定方法和头发请求;服务器回复允许,浏览器也只是决定是否继续跨源调用及暴露响应,业务身份仍由会话/令牌校验。用 curl -i -X OPTIONS -H 'Origin: https://portal.example.test' -H 'Access-Control-Request-Method: POST' https://localhost:9443/procurement/api/requests/123/approve 可以观察拟议部署后的预检响应头;但命令行客户端不会执行浏览器的 CORS 策略,即使返回不允许,curl 仍能发送 POST。要判定防护,需在受控浏览器记录是否向脚本暴露了跨源响应,并同时在服务器端查询申请是否意外改变。未授权源不能靠“看不到结果”证明没有副作用,真正的拒绝必须由认证、CSRF 与对象许可处理。
配置允许源时应精确匹配受信 HTTPS origin(scheme、host、port),不能仅凭字符串后缀 endsWith("example.test") 放行伪造域名;涉及 Cookie 的响应还要显式限制可携带凭据的源、方法、头和缓存行为。反向代理若更改 Host 或协议,实际生效的 Origin 与 TLS 配置须在部署环境核对。当前 WAR 没有该正式受保护路由,以上 OPTIONS 命令和浏览器实测仍 NOT_RUN,不能把公开的教学入口接入互联网来模拟攻击。
每一项攻击各有不同的失败证据
CSRF 的负例必须用有权审批员的真实 Cookie,只去掉或篡改令牌,并核对服务端没有调用审批用例;若用匿名 Cookie,请求在身份校验层就失败,不能拿它证明 CSRF 保护。XSS 负例要让合成恶意供应商名经过数据库、REST/页面模板与浏览器 DOM,而不是只对输入字符串跑转义函数;响应源中的编码字符是否最终成为可执行节点,要在真实浏览器验证。SQL 注入输入的负例需保留参数值和最终查询行,确认另一租户数据未泄漏;如果根本没有搜索入口,curl 的 404 并不算挡住注入。CORS 负例要同时看浏览器是否读取响应和服务器端状态是否改变,不能把两件事合成一个布尔值。
这些实验的先行条件是可信登录、对象授权和冻结的会话机制。若采用 Basic 而非 Cookie,所谓“浏览器自动带 Cookie”的 CSRF 假设不一定适用;若采用 Cookie 但同时允许另一种未受限头部凭据,请分别测各入口。会话 ID 轮换与 CSRF token 轮换是两个动作,旧会话不应自动继续审批,新页面获得新 token 后仍要做对象许可检查。权限服务和 CSRF Filter 当前均不存在,因此以上请求路径尚不能拿来证明防护有效,结论只能是 NOT_RUN。
SQL 排序示例同样有负面分支:传入 "amount; DROP TABLE purchase_request" 应在服务端白名单映射时被拒,不能作为 SQL 片段进入 ORDER BY;合法 amount 映射只生成固定的 total DESC, id DESC。PreparedStatement 再绑定可信租户与状态,查询结果另查库核对。即使日志显示没有 SQL 报错,还要确认不曾为了方便把请求头的租户直接填进绑定参数。各层防护独立验收,才能回答攻击实际停在哪一道边界。
四类攻击各自的输入、浏览器观察与数据库终态见攻防实验卡;其中没有已实施的安全测试结果。
两道有解的练习
练习一: 允许的 CORS 源发了带 Cookie 的 POST 审批,但没有 CSRF 令牌。是否应允许?
解: 不应。CORS 是浏览器读取跨源响应的约束;身份、令牌及对象许可均须单独通过。拒绝时核对 HTTP 拒绝与申请状态不变。若是明确使用无 Cookie Bearer 的 API,另定凭据和跨源暴露策略,不应把 Cookie 表单的假设偷换过去。
练习二: PreparedStatement 绑定了申请 ID,但排序参数直接拼接到 ORDER BY ;能否宣称查询防注入且防越权?
解: 不能。标识符使用固定字段白名单映射为硬编码 SQL 片段;所有用户值绑定参数,租户值来自认证主体的组织关联,并在 WHERE 中限制。三项分别解决 SQL 结构、数据值与对象归属;仅绑定 ID 不够。
版本与依据
规范以 Jakarta Servlet 6.1、Jakarta Security 4.0、Jakarta RESTful Web Services 4.0 和 Java SE 21 PreparedStatement 为入口。基线 JDK 21/Jakarta EE 11、Open Liberty 26.0.0.5;代码片段尚未与运行时集成,未做浏览器或 SQL 安全测试,示意中的安全策略不等于某个实现的默认配置。






