JSP 注释、HTML 注释与表达式:执行阶段、EL 和 XSS 安全指南

先区分服务器模板和浏览器源码

在标准语法的 .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:outescapeXml 默认为 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、stylesrcdoc 或其他危险 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:urlc: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. 当前官方与权威参考

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语言起作用!

Leave a Reply