Table of Contents
先区分服务器模板和浏览器源码
在标准语法的 .jsp 页面里,<%-- ... --%> 是 JSP 注释,容器在翻译页面时忽略它;<!-- ... --> 是 HTML 注释,属于响应模板文本,会发送给客户端。放在 HTML 注释中的 JSP 表达式、Expression Language(EL)或标签仍可能在服务器端执行,结果随后出现在“查看源代码”里。
因此,HTML 注释不是隐藏数据、禁用服务器代码或防止 XSS 的边界。本指南最后核对于 2026 年 9 月 1 日,示例以 Jakarta Pages 4.0、Servlet 6.1、Expression Language 6.0 和 Jakarta Tags 3.0 为基线,并优先采用 servlet/controller、EL 和 JSTL,而不是 scriptlet。
1. JSP 的翻译阶段与请求阶段
Jakarta Pages 4.0 规范把处理分为两个阶段:容器先把 JSP 翻译并编译成 servlet 类;每次请求再运行该 servlet。容器可以在首次请求时翻译,也可以在部署或构建时预编译。
| 页面元素 | 翻译阶段 | 请求/响应结果 |
|---|---|---|
JSP directive,例如 <%@ page ... %> |
配置页面翻译 | directive 本身不写入响应 |
JSP 注释 <%-- ... --%> |
内容被完全忽略 | 不出现在响应中,内部 EL 和 scripting element 也不运行 |
HTML 注释 <!-- ... --> |
作为 template text 处理 | 注释及其中动态结果会写入响应 |
EL ${...} |
解析为页面实现的一部分 | 在请求阶段按 JSP/EL 规则求值 |
JSP expression <%= ... %> |
翻译为 Java 输出逻辑 | 请求阶段执行并直接写入响应;属于应迁移的 scripting element |
| JSTL/custom tag | 翻译为 tag 调用 | 请求阶段运行,具体输出取决于 tag |
部署后页面是否重新翻译取决于容器、预编译方式和变更检测。不要依赖开发模式的自动重载来证明生产部署正确;记录 WAR、容器和 JDK 的版本与摘要。
2. HTML 注释、JSP 注释和代码注释不是同一种东西
下面只演示行为,不包含私人或用户输入:
<%-- Server-only note: the EL here is ignored: ${1 + 1} --%>
<!-- Client-visible note: the EL result is ${1 + 1} -->
响应中只会留下 HTML 注释,且值通常已经求出:
<!-- Client-visible note: the EL result is 2 -->
Java 的 //、/ ... /、JavaScript/CSS 注释和 HTML 注释只对各自语言的解析器有意义。它们不能替代 JSP 注释。JSP document(.jspx)使用 XML syntax,注释和 escaping 规则不同;不要把本页的 standard-syntax 示例未经测试复制到 JSP document。
3. 回答旧问题:可以求值,但不应该用 scriptlet
2011 年原文把 <%= expression %> 放入 HTML 注释。规范允许 HTML 注释作为模板内容包含动态表达式,因此表达式会运行,输出仍对客户端可见。但“语法可行”不等于“设计安全”:表达式可能有副作用、抛出异常、暴露内部状态,原文的 Date#toLocaleString() 也早已过时。
不要用注释包围 scriptlet 来“关闭”代码。HTML 注释不会阻止服务器执行;JSP 注释虽然会忽略内容,但把业务逻辑留在 JSP 仍让测试、编码和升级更困难。把计算、I/O、权限判断和日期格式化移到 controller/service,只把已准备好的显示值放进 request scope。
4. EL、scriptlet 与数据边界
Jakarta Expression Language 6.0区分 ${...} 的立即求值和 #{...} 的 deferred evaluation;具体宿主技术决定何时使用。普通 JSP 视图通常使用 ${...} 读取 scope 中的 bean、map 和属性。
EL 不是 HTML 编码器。直接输出 ${param.name} 不会自动成为安全 HTML,也不应让用户构造 EL 源文本、属性名或方法调用。表达式必须由模板作者固定,用户值只是数据。限制 resolver 暴露的对象和方法,不在视图中做权限决定,也不通过异常或注释泄露内部类、路径、query 或 token。
Scriptlet <% ... %>、declaration <%! ... %> 和 expression <%= ... %>把 Java 混进模板。维护版不使用它们;迁移时先写 characterization test,再把逻辑移到 servlet/controller,最后用 EL/JSTL 替换显示分支和循环。
5. 安全的 servlet/controller 与视图边界
Controller 负责验证、授权和准备显示模型,再 forward 到 WEB-INF 下不能被直接请求的 JSP。下面的数据是可信常量;真实应用不能把“已认证”和“已授权”混为一谈。
package example;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebServlet("/welcome")
public final class WelcomeServlet extends HttpServlet {
@Override
protected void doGet(
HttpServletRequest request,
HttpServletResponse response
) throws ServletException, IOException {
request.setAttribute("message", "Welcome");
request.getRequestDispatcher("/WEB-INF/views/welcome.jsp")
.forward(request, response);
}
}
视图使用 Jakarta Tags 3.0 的标准 core URI。c:out 的 escapeXml 默认为 true,这里显式写出以便审查:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Welcome</title>
</head>
<body>
<%-- Internal implementation note; never place secrets here. --%>
<p><c:out value="${requestScope.message}" escapeXml="true" /></p>
</body>
</html>
应用需要一个与 Jakarta Tags 3.0兼容并锁定版本的实现;Servlet/JSP 容器不一定自带它。不要把 tag library JAR 手工丢进共享服务器目录而不记录来源、版本和许可。
6. 输出编码必须匹配 HTML 上下文
OWASP XSS Prevention Cheat Sheet要求按输出位置选择编码。验证输入格式有助于业务正确性,但不能代替输出编码;CSP 是纵深防御,也不能代替编码。
| 输出位置 | 基线做法 | 停止条件 |
|---|---|---|
| HTML text node | 用 c:out 或可信的 HTML text encoder |
值需要包含受信 HTML;改用经过独立审计的 sanitizer 和类型边界 |
| Quoted HTML attribute | 使用 HTML attribute encoder,并始终加引号 | Event handler、style、srcdoc 或其他危险 attribute |
| URL parameter | 用 URL builder/encoder 构造参数,再对最终 attribute 做 HTML attribute encoding | Scheme 或 origin 可由用户控制 |
| JavaScript string/value | 使用 JavaScript context encoder 或安全 JSON serializer | 尝试把 HTML encoding 当作 JavaScript encoding |
| CSS value | 避免动态 CSS;确需使用时采用 CSS context encoder 和 allowlist | 用户可控制 property、selector 或 URL |
| HTML comment | 不放用户值、secret 或诊断详情 | 值可能包含 comment delimiter,或公开后造成信息泄露 |
c:out适合基础 HTML/XML escaping,但不是所有上下文的通用编码器。OWASP Java Encoder提供按上下文区分的 Java/JSP 编码 API;若采用它,应固定兼容版本并通过依赖扫描。不要使用 escapeXml="false"输出未经审计的字符串。
7. 安全构造 URL,而不是拼接字符串
Jakarta Tags 的 c:url 和 c:param可为应用内路径与参数建立清晰边界,再用 c:out保护 HTML attribute:
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:url var="profileUrl" value="/profile">
<c:param name="id" value="${requestScope.publicUserId}" />
</c:url>
<a href="<c:out value="${profileUrl}" escapeXml="true" />">Profile</a>
这个示例不授权访问,也不允许外部 scheme。Controller 仍需验证 ID 并执行对象级授权。Redirect、下载、javascript: URL、开放重定向和跨 origin 目标需要单独 allowlist;不能因为经过 URL encoding 就视为可信。
8. 注释不是秘密存储或调试通道
HTML 注释会进入 response、CDN、代理、浏览器缓存、录屏和测试 artifact。不要输出用户名、email、内部 host、文件路径、stack trace、SQL、feature flag、版本漏洞线索、session ID、token 或权限判断。即使使用 c:out,信息仍然公开。
JSP 注释不会进入 response,但会留在 source、WAR、source map、backup 和代码审查系统,所以也不能保存 credential。结构化日志只记录最小必要字段,通过 server-side request ID 关联;清除或散列个人标识,禁止记录 cookie、authorization header 和 request body secret。
WHATWG HTML comment syntax还限制 comment 内容。用户值可能破坏 delimiter 或产生不同 DOM;最安全的规则是不把不可信动态值放进 HTML 注释。
9. 最小可执行行为样例
把下面文件保存为 comment-demo/index.jsp。它只用 JSP 内置 EL,不依赖 JSTL,便于验证容器边界;生产视图仍应采用上一节的编码方案。
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" %>
<%-- Server-only marker: this value is ignored: ${1 + 1} --%>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSP comment boundary test</title>
</head>
<body>
<!-- Public diagnostic only: EL result ${1 + 1} -->
<p id="result">EL result: ${1 + 1}</p>
</body>
</html>
常量算术只用于观察阶段差异。不要据此直接输出 request parameter;EL 结果本身没有自动完成 HTML context encoding。
10. 受限容器测试边界
Apache Tomcat 11 文档对应 Jakarta Servlet 6.1 和 Pages 4.0。下面使用 Docker Official Image 的 Tomcat 页面,只绑定 loopback,挂载只读 webapp,并把可写目录限制为临时文件系统。先 pull tag,再记录并运行不可变 digest;未来复现时直接使用记录的 digest。
set -eu
TEST_ROOT="$PWD/comment-demo"
TOMCAT_TAG=tomcat:11.0-jre21-temurin-noble
test -f "$TEST_ROOT/index.jsp"
docker pull "$TOMCAT_TAG"
TOMCAT_IMAGE=$(docker image inspect "$TOMCAT_TAG" --format '{{index .RepoDigests 0}}')
test -n "$TOMCAT_IMAGE"
echo "$TOMCAT_IMAGE"
docker run --rm --name jsp-comment-test --read-only --cap-drop=ALL --security-opt=no-new-privileges --tmpfs /usr/local/tomcat/conf/Catalina --tmpfs /usr/local/tomcat/temp --tmpfs /usr/local/tomcat/work --tmpfs /usr/local/tomcat/logs --publish 127.0.0.1:18080:8080 --volume "$TEST_ROOT:/usr/local/tomcat/webapps/ROOT:ro" "$TOMCAT_IMAGE"
该命令在前台运行;测试完成后用 Ctrl-C 停止,--rm会移除容器。它是本地语义测试,不是 production hardening 模板。不要挂载源码仓库、secret、Docker socket 或生产数据。
11. 验证响应、日志与失败边界
在另一终端请求本地端口。测试要求 HTML 注释包含已求值的 2,JSP 注释 marker 完全不存在:
set -eu
curl --fail --silent --show-error --retry 20 --retry-all-errors --retry-delay 1 http://127.0.0.1:18080/ > response.html
grep --fixed-strings '<!-- Public diagnostic only: EL result 2 -->' response.html
grep --fixed-strings '<p id="result">EL result: 2</p>' response.html
if grep --quiet --fixed-strings 'Server-only marker' response.html; then
exit 1
fi
同时检查容器日志没有 JSP translation/compile error。若响应仍含 ${1 + 1},可能文件未由 JSP servlet 处理、EL 被页面或应用配置禁用、映射错误,或拿到缓存静态文件;不要用 scriptlet 绕过。若 server-only marker 出现,先证明测试的确请求了预期 artifact 和容器,再检查文件 syntax。
测试 artifact 不包含用户数据,但仍应在测试后删除。CI 中记录 JDK、Tomcat image digest、WAR hash、首次请求与后续请求结果,并在支持的目标容器上重复,而不是只测试一个开发服务器。
12. 从 scriptlet 迁移的检查表
- [ ] 为现有页面建立响应、状态码、header 和权限的 characterization test。
- [ ] 把数据库访问、I/O、日期格式化、分支和权限判断移到 service/controller。
- [ ] 只通过最小的 request/view model 向 JSP 暴露显示数据。
- [ ] 用 EL/JSTL 替换 scriptlet 循环和条件,不从用户输入构造 EL。
- [ ] 每个动态输出都按 HTML、attribute、URL、JavaScript 或 CSS 上下文审查。
- [ ] 删除客户端 HTML 注释中的 secret、诊断细节和用户值。
- [ ] 对登录、错误页、国际化、空值、Unicode 和恶意 payload 建立测试。
- [ ] 固定 Jakarta API、Tags implementation、JDK 和容器版本及摘要。
- [ ] 在 staging 预编译或触发每个 JSP,阻止 translation warning/error 上线。
- [ ] 用 response body、浏览器 DOM 和安全扫描共同验证,不能只看页面外观。
13. 当前官方与权威参考
- Jakarta Pages 4.0
- Jakarta Pages 4.0 specification
- Jakarta Servlet 6.1
- Jakarta Expression Language 6.0
- Jakarta Tags 3.0
- WHATWG HTML comments
- OWASP Cross Site Scripting Prevention Cheat Sheet
- OWASP Java Encoder
- Apache Tomcat 11 documentation
- Docker Official Image: Tomcat
14. 2011/2023 年原文精确存档
下面是完整可见的 source_export 正文。原文文字、链接、空白、标点和非标准 comment dash 均未修改;只增加了惰性的外层代码围栏。原始导出和 Git 历史保持不变。
警告:存档使用 scriptlet、过时日期 API,并把动态数据写入客户端可见注释。它仅用于历史审计;请使用上面的维护版指南。
在客户端显示一个注释
Table of Contents
Toggle
- [JSP 语法](https://blog.lazying.art/en/html/computer_internet/java_j2ee_jsp/472/html%e6%b3%a8%e9%87%8a%e4%b8%ad%e6%8f%92%e5%85%a5jsp%e8%a1%a8%e8%be%be%e5%bc%8f.html/#JSP_%E8%AF%AD%E6%B3%95)
- [例子 1](https://blog.lazying.art/en/html/computer_internet/java_j2ee_jsp/472/html%e6%b3%a8%e9%87%8a%e4%b8%ad%e6%8f%92%e5%85%a5jsp%e8%a1%a8%e8%be%be%e5%bc%8f.html/#%E4%BE%8B%E5%AD%90_1)
- [例子 2](https://blog.lazying.art/en/html/computer_internet/java_j2ee_jsp/472/html%e6%b3%a8%e9%87%8a%e4%b8%ad%e6%8f%92%e5%85%a5jsp%e8%a1%a8%e8%be%be%e5%bc%8f.html/#%E4%BE%8B%E5%AD%90_2)
- [总结](https://blog.lazying.art/en/html/computer_internet/java_j2ee_jsp/472/html%e6%b3%a8%e9%87%8a%e4%b8%ad%e6%8f%92%e5%85%a5jsp%e8%a1%a8%e8%be%be%e5%bc%8f.html/#%E6%80%BB%E7%BB%93)
## JSP 语法
<!– comment [ <%= expression %> ] –>
## 例子 1
<!– This file displays the user login screen –>
在客户端的HTML源代码中产生和上面一样的数据:
<!– This file displays the user login screen –>
## 例子 2
<!– This page was loaded on <%= (new java.util.Date()).toLocaleString() %> –>
在客户端的HTML源代码中显示为:
<!– This page was loaded on January 1, 2000 –>
## 总结
这种注释和HTML注释很像,也就是它可以在”查看源代码”中看到。
唯一有些不同的就是,你可以在这个注释中用表达式(例子2所示).这个表达式是不定的,由页面不同而不同,你能够使用各种表达式,只要是合法的就行,更多的请看表达式。
//和/* */在html里也是常用的注释,但只能用在js和CSS语言,不对HTML语言起作用!
