Table of Contents
First separate the server template from browser source
In a standard-syntax .jsp page, <%-- ... --%> is a JSP comment that the container ignores while translating the page. <!-- ... --> is an HTML comment and response template text, so it is sent to the client. A JSP expression, Expression Language (EL) expression, or tag inside an HTML comment may still execute on the server, with its result appearing in “View Source.”
An HTML comment is therefore not a boundary for hiding data, disabling server code, or preventing XSS. This guide was last checked on September 1, 2026. Its baseline is Jakarta Pages 4.0, Servlet 6.1, Expression Language 6.0, and Jakarta Tags 3.0, with servlet/controller, EL, and JSTL preferred over scriptlets.
1. JSP translation and request phases
The Jakarta Pages 4.0 specification defines two phases: the container first translates and compiles a JSP into a servlet class, then runs that servlet for each request. Translation may happen on the first request or during deployment/build precompilation.
| Page element | Translation phase | Request/response result |
|---|---|---|
JSP directive such as <%@ page ... %> | Configures page translation | The directive itself writes nothing to the response |
JSP comment <%-- ... --%> | Body is ignored completely | It is absent from the response; enclosed EL and scripting elements do not run |
HTML comment <!-- ... --> | Handled as template text | The comment and dynamic results within it are written to the response |
EL ${...} | Parsed into the page implementation | Evaluated during the request under JSP/EL rules |
JSP expression <%= ... %> | Translated into Java output logic | Runs during the request and writes directly; it is a scripting element to migrate away from |
| JSTL/custom tag | Translated into tag invocation | Runs during the request; output depends on the tag |
Whether a deployed page is translated again depends on the container, precompilation, and change detection. Do not use development auto-reload as proof of a correct production deployment; record WAR, container, and JDK versions and digests.
2. HTML comments, JSP comments, and code comments are different
This demonstrates behavior only and contains no private or user input:
<%-- Server-only note: the EL here is ignored: ${1 + 1} --%>
<!-- Client-visible note: the EL result is ${1 + 1} -->
Only the HTML comment remains in the response, normally with the value evaluated:
<!-- Client-visible note: the EL result is 2 -->
Java // and / ... /, JavaScript/CSS comments, and HTML comments matter only to their respective parsers. None replaces a JSP comment. JSP documents (.jspx) use XML syntax with different comment and escaping rules; do not copy these standard-syntax examples into a JSP document without a separate test.
3. Answer to the historical question: it evaluates, but do not use a scriptlet
The 2011 source put <%= expression %> inside an HTML comment. The specification permits dynamic expressions in HTML comments as template content, so the expression runs and its output remains visible to the client. But syntactic validity is not safe design: an expression can have side effects, throw an exception, or disclose internal state, and the old Date#toLocaleString() API is obsolete.
Do not surround a scriptlet with HTML comments to “disable” it. HTML comments do not prevent server execution. A JSP comment ignores its body, but leaving business logic in JSP still makes testing, encoding, and upgrades harder. Move computation, I/O, authorization decisions, and date formatting into a controller/service and expose only prepared display values in request scope.
4. EL, scriptlets, and the data boundary
Jakarta Expression Language 6.0 distinguishes immediate ${...} evaluation from deferred #{...} evaluation; the hosting technology determines when each form is used. An ordinary JSP view normally uses ${...} to read beans, maps, and attributes from scopes.
EL is not an HTML encoder. Direct ${param.name} output does not become safe HTML automatically, and users must never construct EL source, property names, or method calls. Expressions stay fixed and author-written; user values are only data. Limit the objects and methods exposed through resolvers, keep authorization decisions out of the view, and do not disclose internal classes, paths, queries, or tokens through exceptions or comments.
Scriptlets <% ... %>, declarations <%! ... %>, and expressions <%= ... %> mix Java into the template. The maintained guide does not use them. During migration, create characterization tests, move logic into the servlet/controller, then replace presentation branches and loops with EL/JSTL.
5. A safe servlet/controller and view boundary
The controller validates, authorizes, and prepares a display model before forwarding to a JSP under WEB-INF, where it cannot be requested directly. The sample data is a trusted constant; a real application must not confuse authentication with authorization.
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);
}
}
The view uses the standard Jakarta Tags 3.0 core URI. c:out defaults escapeXml to true; it is explicit here for review:
<%@ 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>
The application needs a compatible, version-pinned Jakarta Tags 3.0 implementation; a Servlet/JSP container does not necessarily bundle one. Do not manually drop a tag-library JAR into a shared server directory without recording its source, version, and license.
6. Output encoding must match the HTML context
The OWASP XSS Prevention Cheat Sheet requires encoding for the exact output context. Input validation supports business correctness but does not replace output encoding. CSP is defense in depth, not a substitute for encoding.
| Output location | Baseline treatment | Stop condition |
|---|---|---|
| HTML text node | Use c:out or a trusted HTML text encoder | The value must contain trusted HTML; introduce a separately audited sanitizer and type boundary |
| Quoted HTML attribute | Use an HTML attribute encoder and always quote | Event handler, style, srcdoc, or another dangerous attribute |
| URL parameter | Build parameters with a URL builder/encoder, then HTML-attribute encode the final attribute | User controls the scheme or origin |
| JavaScript string/value | Use a JavaScript-context encoder or safe JSON serializer | HTML encoding is being treated as JavaScript encoding |
| CSS value | Avoid dynamic CSS; if essential, use a CSS-context encoder and allowlist | User controls a property, selector, or URL |
| HTML comment | Put no user value, secret, or diagnostic detail there | Value may contain a comment delimiter or disclosure would leak information |
c:out provides basic HTML/XML escaping but is not a universal contextual encoder. OWASP Java Encoder provides context-specific Java/JSP encoding APIs; if adopted, pin a compatible version and scan the dependency. Never use escapeXml="false" for an unaudited string.
7. Build URLs safely instead of concatenating strings
Jakarta Tags c:url and c:param give application paths and parameters a clear boundary, then c:out protects the 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>
This example does not authorize access and does not permit an external scheme. The controller still validates the ID and applies object-level authorization. Redirects, downloads, javascript: URLs, open redirects, and cross-origin destinations need a separate allowlist; URL encoding alone does not make them trusted.
8. Comments are not secret storage or a debugging channel
HTML comments enter the response, CDN, proxies, browser caches, recordings, and test artifacts. Never output usernames, email addresses, internal hosts, file paths, stack traces, SQL, feature flags, vulnerable version clues, session IDs, tokens, or authorization decisions. The information remains public even when c:out encodes it.
A JSP comment is absent from the response but remains in source, WAR files, backups, and code-review systems, so it must not hold credentials either. Structured logs should contain only necessary fields linked by a server-side request ID. Remove or hash personal identifiers and never log cookies, authorization headers, or request-body secrets.
The WHATWG HTML comment syntax also restricts comment content. A user value can disrupt delimiters or produce a different DOM. The safest rule is to put no untrusted dynamic value in an HTML comment.
9. Minimal executable behavior sample
Save this as comment-demo/index.jsp. It uses only built-in JSP EL and no JSTL, keeping the container-boundary test small; production views still need the encoding approach above.
<%@ 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>
Constant arithmetic only demonstrates the phase boundary. It is not permission to output a request parameter directly; an EL result has not automatically received HTML-context encoding.
10. Restricted container test boundary
Apache Tomcat 11 documentation corresponds to Jakarta Servlet 6.1 and Pages 4.0. The following uses the Docker Official Image page for Tomcat, binds only to loopback, mounts the webapp read-only, and limits writable directories to temporary filesystems. Pull the tag, record its immutable digest, then run that digest. Reuse the recorded digest for future reproduction.
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"
The command runs in the foreground. Stop it with Ctrl-C; --rm removes the container. This is a local semantics test, not a production-hardening template. Do not mount the source repository, secrets, Docker socket, or production data.
11. Verify the response, logs, and failure boundary
Request the local port from another terminal. The test requires an HTML comment containing evaluated 2, and no JSP-comment 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
Also check the container log for no JSP translation or compilation error. If ${1 + 1} remains in the response, the file may not be handled by the JSP servlet, EL may be disabled by page/application configuration, routing may be wrong, or a cached static file may have been returned. Do not bypass the failure with a scriptlet. If the server-only marker appears, first prove that the test requested the intended artifact and container, then inspect file syntax.
The test artifact has no user data, but should still be removed after testing. In CI, record the JDK, Tomcat image digest, WAR hash, first-request and later-request results, and repeat on every supported target container instead of testing only one development server.
12. Scriptlet migration checklist
- [ ] Characterize existing response bodies, status codes, headers, and authorization behavior.
- [ ] Move database access, I/O, date formatting, branches, and authorization decisions into services/controllers.
- [ ] Expose only the smallest request/view model needed by the JSP.
- [ ] Replace scriptlet loops and branches with EL/JSTL; never build EL from user input.
- [ ] Review every dynamic output for HTML, attribute, URL, JavaScript, or CSS context.
- [ ] Remove secrets, diagnostics, and user values from client-side HTML comments.
- [ ] Test login, error pages, localization, nulls, Unicode, and malicious payloads.
- [ ] Pin Jakarta APIs, Tags implementation, JDK, container versions, and digests.
- [ ] Precompile or trigger every JSP in staging and block translation warnings/errors from release.
- [ ] Validate response bodies, browser DOM, and security scans together; appearance alone is insufficient.
13. Current official and authoritative references
- 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. Exact archive of the 2011/2023 source
The complete visible source_export body follows. No source text, links, whitespace, punctuation, or nonstandard comment dash was changed; only the inert outer code fence was added. The original export and Git history remain unchanged.
Warning: the archive uses a scriptlet, an obsolete date API, and dynamic data in a client-visible comment. It is retained only for historical audit; use the maintained guide above.
在客户端显示一个注释
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语言起作用!
