c:out 不能防 SQL 注入,因为它仅做 HTML 转义、不影响 SQL 构建;防注入必须用 sql:param 实现参数化查询,但 JSTL SQL 标签库已不推荐用于生产,应改用 PreparedStatement 或 MyBatis 等框架。

为什么 c:out 不能防 SQL 注入
c:out 只做 HTML 转义,把 变成 ,防止 XSS,对 SQL 查询毫无作用。如果你在 JSP 里写 ,再套一层 ,照样被注入——因为 c:out 根本不参与 SQL 构建过程。
常见错误现象:页面显示内容“安全”了,但数据库被删表、拖库、报错 java.sql.SQLException: ORA-00933: SQL command not properly ended。
-
c:out是 JSTL 输出标签,只影响响应体的 HTML 渲染 - SQL 拼接发生在
sql:query或sql:update执行前,此时 EL 表达式已求值完毕 - 真正防注入必须靠参数化查询,即占位符 + 绑定值
用 sql:param 实现参数化查询的正确姿势
sql:param 是 JSTL SQL 库中唯一能将用户输入作为预编译参数传入的标签,它配合 ? 占位符使用,底层调用的是 PreparedStatement.setXXX()。
使用场景:仅适用于简单 CRUD,且数据库连接由 sql:setDataSource 配置;不适用于动态列名、表名或复杂条件拼接。
<setdatasource var="ds" driver="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/test" user="root" password="123"></setdatasource><query datasource="${ds}" var="rs">
SELECT * FROM user WHERE status = ? AND age > ?
<param value="${param.status}">
<param value="${param.age}"></query>
- 每个
?必须对应一个sql:param,顺序严格匹配 -
value属性支持 EL,但不能是表达式链(如${param.x.y}在部分旧版 JSTL 中可能失败,建议先存到 pageScope 变量) - 类型自动推断:字符串进
setString(),数字进setInt();若需指定类型(如java.time.LocalDate),JSTL 不支持,得换原生 JDBC
容易被忽略的兼容性与限制
JSTL SQL 标签库早在 Java EE 5 时代就被标记为“不推荐用于生产”,现在多数容器(Tomcat 10+、Jetty 12)默认不加载 javax.servlet:jstl,更不支持 sql:param。
错误信息示例:org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp/jstl/sql cannot be resolved。
- JSP 页面顶部必须声明:
,且 classpath 下要有jstl.jar和standard.jar(或 Jakarta 版本) - Jakarta EE 9+ 迁移后,URI 变为
https://jakarta.ee/xml/ns/jstl/sql,旧sql:param标签名不变但需新依赖 - 无法处理 NULL 值:当
${param.id}为空时,sql:param会传空字符串而非 SQL NULL,导致查询逻辑错误
真正该做的:别用 JSTL SQL 标签库
不是优化写法的问题,而是整个 sql:* 标签体系缺乏事务控制、连接池管理、异常分类,且无法复用 DAO 层逻辑。现代项目中,所有数据库操作应下沉到 Servlet、Filter 或 Spring Controller 中完成。
替代方案很简单:用 PreparedStatement 手写 JDBC,或引入 MyBatis、JPA 等框架——它们天然支持参数绑定,且 #{} 或 @Param 语法比 sql:param 更可靠、可测、可调试。
如果非要在 JSP 里快速原型,至少确保:① 所有用户输入只进 sql:param,绝不拼 SQL 字符串;② 数据源配置启用预编译(MySQL 加 useServerPrepStmts=true);③ 关闭 JSTL SQL 的 autoCommit(通过 sql:update 的 commit="false" 配合 sql:transaction)。











