SafeHtml & XSS
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against GWT 2.13.1 (released 2026-06-19), gwt-maven-plugin 1.3.0, JDK 21 running the compiler with -sourceLevel 17, the Jakarta Servlet 6 stack of the modular-webapp archetype and the OWASP Java HTML Sanitizer 20260924.2, checked on 2026-10-09.
Cross-site scripting is mostly a client problem in a GWT application: the browser executes JavaScript that the compiler produced from your Java, and the dangerous moment is the one where a string that somebody else controls reaches a DOM property that parses markup.
The safe types described here are shared code (they compile to JavaScript and run on the JVM), the sanitising of stored data belongs to the server, and the Content Security Policy that limits the damage is a build and deployment decision made with the response headers of the host page.
Every output block on this page was produced by running the code: the safe types and the template generator in a lab module compiled with the 2.13.1 compiler and loaded in headless Chrome, the shared classes again on a plain JVM with and without assertions, the OWASP sanitizer on a plain JVM, and the policies against a static server that sets the Content-Security-Policy header.
Where a statement comes only from the Developer Guide, the Javadoc, the release notes or the 2.13.1 source, the text says so.
Two kinds of reader use this page. If you write a view, read Where XSS comes from in a GWT application, The rules in one list and the worked example; those three are the daily rules. If you review or deploy an application, read Content Security Policy with GWT, The server half of the example and Vulnerabilities in GWT itself. The general theory of XSS (contexts, encoding, why a blocklist fails) is on the Thymeleaf page about template security and XSS; the Cross-Origin Resource Sharing background is on CORS. The XSRF, RPC and server security page covers the request-forgery half of the Developer Guide’s security chapter.
Where XSS comes from in a GWT application
A GWT application has no template engine that escapes for you.
You build the user interface from Java, and every API that turns a String into markup is a possible sink.
The compiler does not know which strings are trusted, so the library offers two families of methods: those that take a plain String and interpret it as HTML, and those that take a safe type whose contract says it can be used in an HTML context without running script.
| Sink (unsafe with untrusted data) | Why it is dangerous | Use instead |
|---|---|---|
|
The string is assigned to |
|
|
Same, on a raw DOM element. |
|
|
The constructor parses the string as HTML. |
|
|
A boolean switches the string from text to markup. |
The overload that takes |
|
A |
The |
|
The browser navigates to whatever scheme the string has. |
Check |
JSNI that writes |
Native code sits outside every Java check. |
Do not do it with untrusted data; see JSNI, eval and JSON as sinks |
|
The text is executed as JavaScript. |
|
The Developer Guide page on SafeHtml states the strongest form of the rule: a given HTML widget cannot be an XSS vulnerability as long as the code does not call the String constructors and setHTML(String) methods, and code that uses the equivalent SafeHtml methods is always safe.
That is a statement about the widget, not about the application; the rest of this page is about the places where the contract is easy to break.
The data that reaches those sinks has four common origins in a GWT application:
-
The server. RPC results, JSON or XML documents, a dynamic host page. Stored XSS (a review that another reader wrote) arrives this way.
-
The URL.
Window.Location.getParameter, the hash handled byHistory, the token of aPlace. Reflected and DOM-based XSS arrive this way. -
The user. Text typed into a
TextBoxand shown elsewhere before it is saved. This is self-XSS and is only a problem when the same value is later displayed to somebody else. -
Other windows and frames. Messages from
postMessage, values fromlocalStoragethat another page wrote.
The diagram shows the decision every value passes through before it reaches a sink.
RPC result, JSON, URL token,
user input, stored review"] --> q{"Does it need
to contain markup?"} q -- "no" --> text["Escape or use a text sink
setText, setInnerText,
SafeHtmlUtils.fromString,
TextCell"] q -- "yes" --> san["Sanitise with an allowlist
SimpleHtmlSanitizer on the client,
OWASP sanitiser on the server"] text --> safe["SafeHtml, SafeUri, SafeStyles"] san --> safe safe --> sinks["Safe sinks
HTML(SafeHtml), setInnerSafeHtml,
cells, SafeHtmlTemplates"] src -. "string concatenation" .-> bad["Unsafe sinks
setHTML(String), innerHTML,
href, JSNI, eval"] bad --> xss["XSS"] csp["Content Security Policy
second line of defence"] -. "blocks inline handlers and
foreign scripts" .-> xss
A measured example of the unsafe path
The first output of the lab shows what the unsafe sink does and what the safe one does with the same string.
The string is <img src=x onerror=pwn()> & "q" 'a', where pwn() is a function of the lab’s host page that only sets the document title (an alert would block a headless browser):
Element host = Document.get().createDivElement();
host.setInnerHTML(evil); // String is parsed as markup
Element safe = Document.get().createDivElement();
safe.setInnerSafeHtml(SafeHtmlUtils.fromString(evil)); // String is escaped first
innerHTML(untrusted) => 2 nodes, first=IMG
setInnerSafeHtml(fromString) => 1 text node: <img src=x onerror=pwn()> & "q" 'a'
document.title after load => pwn calls: 1
The img element was created and its onerror handler ran once, from a detached element that was never added to the page.
The escaped version produced a single text node that shows the characters.
The title change is the proof of execution; the same code with alert(document.cookie) would have been a session theft.
The safe types
GWT’s answer is a family of small types, which the Developer Guide labels as new in GWT 2.1 and which live in three packages.
Each is an interface or a class whose documentation states a contract, and the constructors that skip the checking carry the word Trusted or Constant in their names so that a code review can search for them.
| Type | Package | Contract |
|---|---|---|
|
|
|
|
|
|
|
|
A sequence of |
|
the same packages |
Accumulate pieces; text is escaped, constants are checked in development mode, other |
|
the same packages |
Static factories: escaping, sanitising and the explicit "I vouch for this string" methods. |
|
|
A tag interface; the compiler generates an implementation that escapes every parameter for the position where it appears. |
|
|
Implemented by widgets with a |
|
|
Turn a |
The annotations @IsSafeHtml, @IsSafeUri and @IsTrustedResourceUri in com.google.gwt.safehtml.shared.annotations mark String parameters that must receive an already-safe value; the library’s own methods are annotated with them (HTML.setHTML(@IsSafeHtml String), Anchor.setHref(@IsSafeUri String), Frame.setUrl(@IsTrustedResourceUri SafeUri)).
The GWT compiler does not enforce them.
They exist for static checkers such as Error Prone plug-ins, and they document the contract in your own APIs too.
The Javadoc of SafeHtml adds a rule that is easy to miss.
SafeHtml is for protecting the browser.
It must not be used to "clean" input before sending it to the server, because the server cannot trust the type of a value it receives: a malicious client can send a crafted serialised form of any implementation, and SafeHtml is Serializable.
Validate and sanitise on the server again (see The server half of the example).
SafeHtmlUtils and SafeHtmlBuilder
SafeHtmlUtils has three families of methods.
htmlEscape(String), htmlEscape(char) and htmlEscapeAllowEntities(String) return a String (the char version returns the character or its entity; the last one keeps well-formed entities).
fromString(String) escapes and wraps.
fromSafeConstant(String) and fromTrustedString(String) wrap without escaping and are the audit points: fromSafeConstant is for a compile-time constant that ends in "inner HTML" context, and fromTrustedString is the general "this string is known to satisfy the contract" escape hatch that the Javadoc strongly discourages.
SafeHtmlUtils.EMPTY_SAFE_HTML is the shared empty value.
The lab fed the same string, <img src=x onerror=pwn()> & "q" 'a', to the escaping methods:
htmlEscape(evil) => <img src=x onerror=pwn()> & "q" 'a'
fromString(evil).asString() => <img src=x onerror=pwn()> & "q" 'a'
htmlEscapeAllowEntities("a & b < <c> &bogus") => a & b < <c> &bogus
Five characters are escaped: &, <, >, " and '.
htmlEscapeAllowEntities does not double-escape a syntactically valid entity (&, <), but escapes an ampersand that does not start one (&bogus became &bogus).
Use it only for text that is already partly encoded, such as the output of SimpleHtmlSanitizer.
SafeHtmlBuilder assembles markup from fixed pieces and escaped pieces.
Its methods say which is which: appendEscaped(String) and appendEscapedLines(String) escape (the second turns newlines into br), append(SafeHtml) adds a safe value as it is, the append overloads for numbers and char add their text form, and appendHtmlConstant(String) adds markup that you wrote in the source:
SafeHtml built = new SafeHtmlBuilder()
.appendHtmlConstant("<b>")
.appendEscaped(book.getTitle()) // "Dune <script>x</script>"
.appendHtmlConstant("</b>")
.appendHtmlConstant("<br>")
.appendEscapedLines(book.getNotes()) // "line1\nline<2>"
.append(42)
.toSafeHtml();
<b>Dune <script>x</script></b><br>line1<br>line<2>42
The constant rule and its development-mode check
appendHtmlConstant and fromSafeConstant have two conditions.
The argument must be fully determined at compile time, so that nothing a user typed can end up in it, and it must end in "inner HTML" context: it may not stop inside a tag or an attribute.
The Javadoc gives the standard counter-example:
// WRONG: the anchor tag is incomplete when the argument ends
new SafeHtmlBuilder().appendHtmlConstant("<a href='").append(url); // do not write this
The second condition is checked, but only where checking is cheap.
In client code the check runs in development mode only, and it is skipped in the compiled JavaScript.
On the JVM it runs when assertions are enabled, or when the system property com.google.gwt.safehtml.ForceCheckCompleteHtml is set, or after SafeHtmlHostedModeUtils.setForceCheckCompleteHtml(true).
The same four calls on a JVM, without and with -ea, and with the property:
$ java -cp ... Jvm # no assertions
no error # appendHtmlConstant("<a href='")
no error # fromSafeConstant("<b>unclosed <i")
no error # SafeStylesUtils.fromTrustedString("color: red")
safe: "<b>a</b><b onclick=x>b</b></i>'&bogus;"
$ java -ea -cp ... Jvm
java.lang.AssertionError: String is not complete HTML (ends in non-inner-HTML context): <a href='
java.lang.AssertionError: String is not complete HTML (ends in non-inner-HTML context): <b>unclosed <i
java.lang.AssertionError: Invalid CSS Property: 'color: red'. CSS properties must be an empty string or end with a semi-colon (;).
$ java -Dcom.google.gwt.safehtml.ForceCheckCompleteHtml=true -cp ... Jvm
java.lang.IllegalArgumentException: String is not complete HTML (ends in non-inner-HTML context): <a href='
java.lang.IllegalArgumentException: String is not complete HTML (ends in non-inner-HTML context): <b>unclosed <i
no error # the style check is an assertion only
Run your unit tests with assertions enabled (Surefire does by default) and the mistake is found before production.
The compiled application accepted the incomplete constant silently: the lab’s production-mode build printed accepted (production mode skips the check).
SafeHtmlTemplates
String concatenation is what SafeHtmlBuilder avoids, but a builder is verbose for anything with attributes.
SafeHtmlTemplates lets you write the markup once, with numbered placeholders, and have the compiler generate the escaping.
The template is the value of a @Template annotation; every parameter is escaped, sanitised or passed through according to its type and its position in the markup.
The Bookshelf lab uses one for the review box of The Bookshelf worked example: reader reviews:
public interface ReviewTemplates extends SafeHtmlTemplates {
@Template("<article class=\"review\"><h4>{0}</h4><p style=\"{3}\">{1}</p>"
+ "<a href=\"{2}\">Reviewer site</a></article>")
SafeHtml review(String reviewer, SafeHtml body, SafeUri site, SafeStyles style);
@Template("<a href=\"{0}\" title=\"{1}\">link</a>")
SafeHtml rawLink(String href, String title);
}
private static final ReviewTemplates T = GWT.create(ReviewTemplates.class);
GWT.create runs the SafeHtmlTemplatesGenerator at compile time (it is deferred binding; see Deferred binding and generators).
The generated class, taken from the compiler’s -gen directory, shows what the annotation became:
public SafeHtml review(String arg0, SafeHtml arg1, SafeUri arg2, SafeStyles arg3) {
StringBuilder sb = new java.lang.StringBuilder();
sb.append("<article class=\"review\"><h4>");
sb.append(SafeHtmlUtils.htmlEscape(arg0));
sb.append("</h4><p style=\"");
sb.append(SafeHtmlUtils.htmlEscape(arg3.asString()));
sb.append("\">");
sb.append(arg1.asString());
sb.append("</p><a href=\"");
sb.append(SafeHtmlUtils.htmlEscape(arg2.asString()));
sb.append("\">Reviewer site</a></article>");
return new OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml(sb.toString());
}
public SafeHtml rawLink(String arg0, String arg1) {
StringBuilder sb = new java.lang.StringBuilder();
sb.append("<a href=\"");
sb.append(SafeHtmlUtils.htmlEscape(UriUtils.sanitizeUri(arg0)));
sb.append("\" title=\"");
sb.append(SafeHtmlUtils.htmlEscape(arg1));
sb.append("\">link</a>");
return new OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml(sb.toString());
}
The rules the generator applied, as the 2.13.1 source states them:
| Parameter type | Position | Treatment |
|---|---|---|
|
text content or a quoted attribute value |
HTML-escaped |
|
a URL attribute ( |
|
|
text content |
appended as it is |
|
an attribute |
compile error |
|
the whole value of a URL attribute |
HTML-escaped only (the value is already trusted) |
|
a part of an attribute value, or text content |
compile error |
|
the whole value of a |
HTML-escaped |
|
a |
HTML-escaped, with a compile warning: the generator cannot guarantee safety in a CSS context |
any |
a |
compile error |
The lab output for a hostile input, from T.rawLink("javascript:alert(1)", "t\" onmouseover=\"x") and a harmless one:
<a href="#" title="t" onmouseover="x">link</a>
<a href="https://example.org/a?b=1&c=2" title="ok">link</a>
Source: GWT Javadoc — SafeHtmlTemplates
The javascript: URL became # and the quote that tried to open a new attribute became ".
Compiling rawLink printed the INFO line that tells you the URL is being sanitised on your behalf:
Template with variable in URL attribute context: The template code generator will sanitize the URL. Use SafeUri to specify arguments in a URL attribute context that should not be sanitized.
Templates that the generator refuses
The generator’s parser is lenient about HTML syntax, but it refuses templates in which a placeholder is in a position that cannot be escaped safely.
Ten deliberately bad interfaces in a probe module produced these messages, each followed by Failed to resolve '...' via deferred binding:
| Template or declaration | Message |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
a method without |
|
a method that returns |
|
|
|
Two things are worth remembering from the table.
A template is a compile-time asset, so the errors appear when you build, not when a reader opens the page.
And a String in a style attribute is the one case where the generator warns and continues; treat that warning as an error in your build output and pass SafeStyles.
Templates are also the engine under UiBinder (see UiBinder).
The configuration property UiBinder.useSafeHtmlTemplates is true by default in 2.13.1 (the module file calls it deprecated, discourages disabling it and announces its removal), so the HTML that a ui.xml file produces is escaped the same way, and ui:text and ui:safehtml in a template choose between an escaped text value and a SafeHtml value (see UiBinder).
The 2.3.0 release notes describe the integration as off by default, with a build warning that your users were left "more vulnerable to cross-site scripting attacks" and a known-issues list; tutorials from that period show the property being switched on by hand, and the default has changed since.
SafeUri and UriUtils
A URL is dangerous because its scheme can be executable: javascript: runs script when a link is followed or a frame is loaded, and data:text/html can render a page in the origin of some browsers' navigation targets.
UriUtils is the toolbox.
| Method | Behaviour |
|---|---|
|
true when the URI has no scheme, or the scheme is |
|
returns |
|
returns a |
|
wraps a compile-time constant without checking |
|
wraps without checking; the audit point |
|
deprecated; wraps without any check |
|
the scheme, or |
|
percent-encode; the second leaves existing |
sanitizeUri on a range of inputs in the lab (the second column is isSafeUri, the third extractScheme):
sanitizeUri(https://example.org/a b?q=1&r=2) => https://example.org/a%20b?q=1&r=2 safe=true scheme=https
sanitizeUri(/relative/path) => /relative/path safe=true scheme=null
sanitizeUri(mailto:a@example.org) => mailto:a@example.org safe=true scheme=mailto
sanitizeUri(javascript:alert(1)) => # safe=false scheme=javascript
sanitizeUri(JaVaScRiPt:alert(1)) => # safe=false scheme=JaVaScRiPt
sanitizeUri( javascript:alert(1)) => # safe=false scheme= javascript
sanitizeUri(data:text/html,<b>x</b>) => # safe=false scheme=data
sanitizeUri(vbscript:x) => # safe=false scheme=vbscript
sanitizeUri(//evil.example/x) => //evil.example/x safe=true scheme=null
sanitizeUri(http://example.org/"onmouseover="x) => http://example.org/%22onmouseover=%22x safe=true scheme=http
UriUtils.fromString("javascript:alert(1)").asString() => #
Four consequences follow from the output.
-
The check is an allowlist of schemes, so unusual and mixed-case spellings of
javascript:fail it, and so does a leading space (the scheme becomes ` javascript`, which is not on the list). -
The characters that could end an attribute value are percent-encoded, so a URL cannot break out of
href="...". -
A scheme-relative URL such as
//evil.example/xis "safe" by this definition because it has no scheme.sanitizeUriprotects against script execution; it does not decide which hosts you trust. A link that must stay on your site, or that must go only to a known set of hosts, needs its own check on top. -
The replacement for a rejected URL is
#, which a user sees as a link that does nothing. Log or reject the input on the server if you can; a silent#hides the attack from you as well.
For navigation by code (Window.Location.assign, Window.open) the same rule applies, but no overload takes a SafeUri.
Call UriUtils.isSafeUri(url), and where only web pages are acceptable compare extractScheme with http and https yourself, before the call.
URLs that arrive in a JSON document from another service (a "buy" link, an image address) are the usual victims, because the developer who wrote the page trusted the service.
SafeStyles
SafeStyles plays the same role for CSS declarations that end up in a style attribute or in Style properties that take a string.
Its factories are SafeStylesUtils (static methods named forWidth, forHeight, forFontSize, forFontWeight, forDisplay and many more, each taking a value and a Unit or an enum) and SafeStylesBuilder (the same properties as chained methods).
The methods that accept a free-form string are named forTrusted..., trusted... and fromTrustedString.
Only typed values and SafeUri are possible through the typed methods, so a number or an enum cannot carry a payload.
SafeStyles st = new SafeStylesBuilder()
.width(50, Unit.PCT)
.trustedColor("#336699")
.fontWeight(Style.FontWeight.BOLD)
.toSafeStyles();
SafeStyles h = SafeStylesUtils.forHeight(2.5, Unit.EM);
styles => width:50.0%;color:#336699;font-weight:bold;
stylesUtils => height:2.5em;
Two caveats come from the documentation of the trusted methods.
A trusted value may never contain angle brackets, or a SafeStyles placed in a style element could break out of the CSS context into HTML, and the string must end with a semicolon or be empty (the assertion message was shown above).
And a hostile value in a CSS declaration is still dangerous in other ways: background-image: url(...) makes the browser request a URL of the attacker’s choosing, which is a tracking and data-exfiltration primitive, so give forBackgroundImage a SafeUri and do not build declarations from user text at all.
Widgets, cells and the SafeHtml overloads
HasSafeHtml is the interface that widgets implement to accept markup safely: one method, setHTML(SafeHtml).
The widgets that take HTML have gained an equivalent that takes SafeHtml next to each String method, and the safe overload is the one to call:
HTML html = new HTML(SafeHtmlUtils.fromString(userText)); // escaped
Anchor link = new Anchor(SafeHtmlUtils.fromString(label), UriUtils.fromString(url));
Button b = new Button(safeHtml, clickHandler);
MenuItem item = new MenuItem(safeHtml, command);
HTMLPanel panel = new HTMLPanel(safeHtml);
CheckBox box = new CheckBox(safeHtml);
Image img = new Image(UriUtils.fromString(url));
Tree tree = new Tree();
tree.addTextItem(book.getTitle()); // text, never markup
Source: GWT Javadoc — HasSafeHtml
Where the goal is text, use the text method (Label.setText, Element.setInnerText, Tree.addTextItem, Document.createTextNode) and the question of escaping does not arise.
Tree.addItem(String) and insertItem(int, String) no longer exist in 2.13.1; the 2.5.0 RC1 release notes explain that they were deprecated because of "potential XSS issues" and name addTextItem and insertTextItem as the replacement.
The Developer Guide asks library authors for one more habit: unwrap a SafeHtml with asString() as late as possible, immediately before the innerHTML assignment, and pass SafeHtml objects between components rather than strings.
Cells
The cell widgets (see Cell widgets) render a row to a SafeHtmlBuilder, so they are built on the same types.
TextCell and ClickableTextCell extend AbstractSafeHtmlCell<String> and render through SimpleSafeHtmlRenderer, which escapes; SafeHtmlCell takes a SafeHtml value and appends it unchanged.
With the hostile string of the lab:
TextCell => <img src=x onerror=pwn()> & "q" 'a'
ClickableTextCell => <img src=x onerror=pwn()> & "q" 'a'
SafeHtmlCell => <b>Dune <script>x</script></b><br>line1<br>line<2>42
So a TextColumn is safe for any string, and a SafeHtmlCell column is safe exactly as far as the SafeHtml values you give it.
A custom cell extends AbstractCell<C> and writes into the SafeHtmlBuilder that is passed to render; use sb.appendEscaped(...) or a SafeHtmlTemplates call there, never sb.appendHtmlConstant with data.
The example of The Bookshelf worked example: reader reviews is such a cell.
The SafeHtmlRenderer<T> interface (in com.google.gwt.text.shared) is the way to plug a different rendering into the standard cells; AbstractSafeHtmlRenderer<T> is the base class to extend.
The rules in one list
-
Show data with a text method whenever you can. It cannot be an XSS sink.
-
When you need markup, build it from constants and escaped values with
SafeHtmlBuilderor from aSafeHtmlTemplatesinterface. Never assemble markup by concatenating strings. -
Take
SafeHtml,SafeUriandSafeStylesin the signatures of your own views and presenters, so that unwrapping happens in one place. -
Treat every use of
fromTrustedString,fromSafeConstant,appendHtmlConstant,forTrusted...,trusted...andunsafeCastFromUntrustedStringas a line that needs a reviewer’s signature, and search for those names in code review and in CI. -
Never put a user value in a JSNI snippet, an
evalor aFunction. -
Validate URLs with
UriUtils; for navigation by code checkisSafeUriand the host you expect. -
Keep a Content Security Policy as the second line of defence (see Content Security Policy with GWT).
-
Do all of this again on the server for stored data (see The server half of the example).
Sanitising user HTML
Sometimes the user is supposed to enter markup: a review with bold text, a comment with a list. Escaping would show the tags, so the markup has to be sanitised against an allowlist of elements and attributes.
SimpleHtmlSanitizer on the client
SimpleHtmlSanitizer (which implements HtmlSanitizer) is the sanitiser that ships with GWT.
It is deliberately tiny.
It accepts only attribute-free tags from a fixed set: b, em, i, strong, br, hr, h1 to h6, ul, ol and li, plus numeric entities and entity references, and it escapes everything else:
SafeHtml san = SimpleHtmlSanitizer.sanitizeHtml(
"foo < bar & that is <em>good</em>, <span style=\"foo: bar\">x</span> "
+ "<b onclick=\"x()\">y</b> <script>z</script>");
foo < bar & that is <em>good</em>, <span style="foo: bar">x</span> <b onclick="x()">y</b> <script>z</script>
The span and the script were shown as text.
The b element with an onclick attribute was not allowed, because the allowlist covers tags without attributes; its opening tag was escaped, but the matching </b> closing tag, which is on the list, was kept.
That is the limit the Javadoc states: the sanitiser does not promise well-formed or balanced HTML.
An unbalanced closing tag cannot execute script, but it can close an element of your own layout early, so render sanitised output inside an element of its own.
sanitizeHtml(null) throws NullPointerException: html is null.
A JVM run of <b>a</b><b onclick=x>b</b></i>'&bogus; gave <b>a</b><b onclick=x>b</b></i>'&bogus;.
There is no method to sanitise an existing SafeHtml.
The class comment explains why: such a method could return values that are not stable, so that sanitize(s.asString()) differs from s.
SimpleHtmlSanitizer fits snippets such as search results with b tags around the match.
It does not handle links, images or styles at all, and it does not understand HTML the way a browser does.
For anything richer use a real sanitiser.
A real sanitiser on the server
The standard Java answer is the OWASP Java HTML Sanitizer (com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer, 20260924.2 on Maven Central on 2026-10-09).
It builds a policy from an allowlist of elements, attributes and URL protocols, and it runs on the server, so it can be used by the servlet that stores the review.
It is not GWT-compatible and cannot be called from client code.
PolicyFactory policy = new HtmlPolicyBuilder()
.allowElements("b", "i", "em", "strong", "p", "br", "ul", "ol", "li", "a")
.allowUrlProtocols("https")
.allowAttributes("href").onElements("a")
.requireRelNofollowOnLinks()
.toFactory();
String safe = policy.sanitize(untrusted);
The lab ran that policy on three inputs and on the prepackaged Sanitizers.FORMATTING.and(Sanitizers.LINKS):
in : Great <b>read</b> <img src=x onerror=pwn()> <script>pwn()</script>
out: Great <b>read</b>
in : <a href="javascript:pwn()">click</a> <a href="https://example.org/x" onclick="pwn()">ok</a>
out: click <a href="https://example.org/x" rel="nofollow">ok</a>
in : <p style="background:url(x)">styled</p> <b onclick=x>y
out: <p>styled</p> <b>y</b>
The disallowed elements and the content of the script element were dropped, the javascript: link lost its href, event handler and style attributes were removed, the allowed link got rel="nofollow", and the unclosed b was closed.
The output is well formed, which the simple sanitiser does not promise.
Its project page describes the library as protecting against XSS in HTML authored by third parties, and the project README gives the same builder API.
Where to put the sanitiser:
-
Sanitise on write on the server, so that the stored value is already clean, and keep the policy next to the code that stores it.
-
Sanitise again on read in the client with
SimpleHtmlSanitizeror escaping if the value is only ever shown, as a second line of defence against data that entered the database some other way (an import, a migration, an old version of the application). -
Convert to
SafeHtmlat the very last moment, withSafeHtmlUtils.fromTrustedStringonly at the line that follows a sanitising call, and comment it with the name of the policy. The server’s output is a plainString; the cast is where your claim "this is safe" becomes code.
JSNI, eval and JSON as sinks
JSNI (see JSNI and overlay types) is JavaScript inside a Java comment.
The Java type system and the safe types stop at its edge, so a JSNI method that assigns a String to innerHTML, to location, to a script src or to the argument of eval, new Function, setTimeout(string) or document.write is an XSS sink whatever the Java caller does.
Prefer the typed Element API, Window.Location, JsInterop with Elemental2 (see DOM and Elemental2) and ScriptInjector, and keep any JSNI that takes strings away from data that a user influenced.
The same holds for the code behind the -*/ of a native method that someone pastes from a tutorial.
Data formats have the same shape.
The 2.13.1 JsonUtils has two evaluators:
-
JsonUtils.safeEval(String)parses withJSON.parseand throwsIllegalArgumentExceptionon text that is not valid JSON. It returns an object or array, not a primitive. Use this one. -
JsonUtils.unsafeEval(String)executes the text as JavaScript. Its use on anything a third party can influence is code execution, not parsing.
JSONParser.parse and parseLenient in com.google.gwt.json.client are deprecated for the same reason (the strict one is parseStrict).
The JSON chapters of this section (RequestBuilder, JSON and XML) show the safe call, and the GWT-RPC page (GWT-RPC) explains why the default RPC wire format, which is evaluated by the client, interacts with the Content Security Policy.
Content Security Policy with GWT
A Content Security Policy (CSP) is a response header that tells the browser which sources of script, style, images and frames are allowed.
It does not replace escaping, but it turns a successful injection into a blocked script: a policy without unsafe-inline refuses inline event handlers such as onerror=... and inline script elements, even if the markup got into the page.
GWT applications can run under a strict policy, with some care in four places.
What the lab measured
The lab module (compiled with the default linker, xsiframe) was loaded in headless Chrome from a host page with no inline script, under five policies, and the page tried the unsafe innerHTML sink of Where XSS comes from in a GWT application in each.
pwn() is defined in an external file pwn.js; the nonce values are placeholders (NONCE_PLACEHOLDER).
| Policy header | Module starts | Injected handler ran | What else happened |
|---|---|---|---|
none |
yes |
yes (title changed) |
|
|
yes |
no |
The inline |
|
yes |
no |
Style attributes in markup ( |
|
yes |
no |
Styles worked again (computed |
|
yes |
no |
|
|
no |
not reached |
The module never started |
The last row failed with this message in the browser console:
Loading the script 'http://localhost:9941/lab/PERMUTATION.cache.js' violates the following Content Security Policy directive: "script-src 'nonce-NONCE_PLACEHOLDER'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback. The action has been blocked.
The cause is the structure of the xsiframe linker.
The nocache.js bootstrap creates a hidden iframe (it was visible in the dumped DOM as <iframe id="lab" ...>) and the permutation script is loaded into it.
The nonce of your host page is not on that script, so without 'self' or 'strict-dynamic' the browser refuses it.
'strict-dynamic' makes scripts loaded by a nonced script trusted, which is the shape of a GWT bootstrap.
A policy to start from
The measurements give a starting point for a GWT host page that has no inline script, loads the bootstrap from its own origin and talks only to its own server.
The lab module started under exactly this policy, the unsafe innerHTML of Where XSS comes from in a GWT application ran no handler, and inline styles worked:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
object-src 'none';
base-uri 'none';
frame-ancestors 'self'
Each directive has a reason that comes from GWT.
style-src 'unsafe-inline' is needed because StyleInjector and CssResource.ensureInjected() write inline style elements, because markup with style attributes (such as the template output above) is subject to style-src, and because the StyleInjector source has no nonce support.
img-src data: is needed because ClientBundle inlines small images as data URLs (see ClientBundle and images).
script-src 'self' works for the lab’s output style because the nocache script, the permutation files and the policy-compatible RPC format come from your own origin (see the note on output styles below).
A host page that must contain an inline script, for example a dynamic host page with initial data, should use a nonce or a hash for that block, or the type="application/json" data-block pattern that the deployment page shows (Server integration and deployment).
The output style decides whether this holds. The default obfuscated output makes the xsiframe linker install each permutation as inline script text (script.text), so script-src 'self' alone stops such a module with an inline-script message, and the GWT-RPC lab saw exactly that. The module of this lab was compiled with -style PRETTY, for which the code is loaded from the permutation file, and it started under script-src 'self'; the direct_install and sso linkers start under that policy as well. With the obfuscated default, add 'unsafe-inline' or use a nonce together with 'strict-dynamic'. The Linkers and bootstrap page has the measured matrix of output styles, linkers and policies. In this lab the same message ("Executing inline script violates the following Content Security Policy directive 'script-src 'self''…") appeared only when the code called ScriptInjector.fromString, so look for inline script in the host page or in code that injects text scripts when you see it.
If your host page has to use a nonce, give it to both the bootstrap tag and the data tag, add 'strict-dynamic', and generate a new unpredictable nonce for every response on the server:
<script nonce="CSP_NONCE_PLACEHOLDER" src="bookshelf/bookshelf.nocache.js"></script>
Source: MDN — Content Security Policy
The nonce here is a placeholder: it must come from a secure random generator and change on every response, and never be a constant in a file.
ScriptInjector and the nonce
ScriptInjector is the class for loading JavaScript at runtime (fromUrl and fromString).
Its source, in nativePropagateScriptNonceIfPossible, looks for the first script element with a nonce attribute in the document and copies that nonce to the script element it injects.
This is why fromString worked under the nonce policy and failed under script-src 'self' in the table above.
It also means that the injected script gets the nonce of whatever the first nonced script is, so keep untrusted markup out of the document head.
The behaviour is in the source and in the lab; the Javadoc does not describe it, and there is no nonce support in StyleInjector.
Widgets that need a workaround
The module com.google.gwt.user.User defines a binding property gwt.cspCompatModeEnabled (values true and false, default false).
The comment in UI.gwt.xml says it makes the widgets try to be compatible with strict CSP policies, for example by preventing the javascript:; default href of an Anchor from triggering an inline-script violation when the anchor is clicked, and that it "should be safe to enable and should have no user-visible effects".
Set it in your module XML:
<set-property name="gwt.cspCompatModeEnabled" value="true"/>
Source: GWT 2.13.1 source — UI.gwt.xml
CellTree already prevents the strict-CSP violation of its javascript: link target in 2.13.1 and cancels the default action of its links, as the comments in the source say.
For your own code: do not use javascript: URLs, and call preventDefault() in the click handler of an Anchor that acts as a button.
The JUnit support in JUnit.gwt.xml has a separate property, gwt.strictCspTestingEnabled, that makes GWTTestCase runs under JUnitShell add a Content-Security-Policy header (a nonce policy with strict-dynamic) to the module’s files so that a test fails when a widget violates it.
What the release notes say about CSP
-
2.8.1 and 2.8.2 list "improvements for compliance with strict CSP" and "improved compliance with CSP" among the miscellaneous changes.
-
2.12.0 (2024-10-29) lists "Fix CSP issues in linkers and dev mode, provide CSP workarounds for GWT-RPC payloads" as a highlight. The notes say no more. What the 2.13.1 sources and the lab show for the RPC part is the
gwt.rpc.versionproperty: with the default protocol version 7 the client evaluates the response, which needs'unsafe-eval'; started with-Dgwt.rpc.version=8the server writes a payload that the client parses withJSON.parse, and no'unsafe-eval'is needed. The GWT-RPC page has the measured case. Do not attribute that property to the release note: the note does not name it. -
Other
evalusers:ExternalTextResourceandJsonUtils.unsafeEvalneed'unsafe-eval'too, as the ClientBundle and images page notes. -
Super Dev Mode loads the compiled code from the code server (
127.0.0.1:9876by default), so a policy on a development host has to allow that origin for script. Do not carry that allowance into production, and never expose the code server; see XSRF, RPC and server security.
The Bookshelf worked example: reader reviews
The Bookshelf shows each book with reviews written by readers. A review has a reviewer name, a body in which a few formatting tags are allowed, a link to the reviewer’s website and a rating of one to five. All four are untrusted. The class below is the lab’s version of the shared review:
public class Review {
private final String reviewer;
private final String body;
private final String website;
private final int rating;
public Review(String reviewer, String body, String website, int rating) {
this.reviewer = reviewer;
this.body = body;
this.website = website;
this.rating = rating;
}
public String getReviewer() { return reviewer; }
public String getBody() { return body; }
public String getWebsite() { return website; }
public int getRating() { return rating; }
}
The cell that displays it uses one template and chooses a safe type for each value:
public class ReviewCell extends AbstractCell<Review> {
interface Templates extends SafeHtmlTemplates {
@Template("<div class=\"review\"><strong>{0}</strong>"
+ "<span class=\"bar\" style=\"{1}\"></span>"
+ "<div class=\"body\">{2}</div><a href=\"{3}\" rel=\"nofollow\">website</a></div>")
SafeHtml review(String reviewer, SafeStyles ratingBar, SafeHtml body, SafeUri website);
}
private static final Templates TEMPLATES = GWT.create(Templates.class);
@Override
public void render(Context context, Review value, SafeHtmlBuilder sb) {
if (value == null) {
return;
}
SafeStyles bar = SafeStylesUtils.forWidth(value.getRating() * 20, Unit.PCT);
SafeHtml body = SimpleHtmlSanitizer.sanitizeHtml(value.getBody());
SafeUri website = UriUtils.fromString(value.getWebsite());
sb.append(TEMPLATES.review(value.getReviewer(), bar, body, website));
}
}
-
The reviewer’s name is a
Stringin text position, so the generator escapes it. -
The rating becomes a width by arithmetic and a typed factory, so no string from the user reaches the
styleattribute. -
The body is sanitised to a
SafeHtmlbefore it reaches the template. -
The website goes through
UriUtils.fromString, which turns ajavascript:URL into#, and the link hasrel="nofollow".
The imports are com.google.gwt.cell.client.AbstractCell, com.google.gwt.core.client.GWT, com.google.gwt.dom.client.Style.Unit, com.google.gwt.safecss.shared.SafeStyles and SafeStylesUtils, and, from com.google.gwt.safehtml.shared, SafeHtml, SafeHtmlBuilder, SafeUri, SimpleHtmlSanitizer and UriUtils, plus SafeHtmlTemplates from com.google.gwt.safehtml.client.
Note that SafeStyles lives in com.google.gwt.safecss.shared, not in the safehtml package; importing it from the wrong package is an easy mistake (the first compile of this lab failed that way with "SafeStyles cannot be resolved to a type").
The lab rendered a hostile review and a harmless one with new ReviewCell().render(null, review, sb):
<!-- Review("Mallory <img src=x onerror=pwn()>", "Loved it <b>a lot</b><script>pwn()</script> <a href=\"javascript:pwn()\">x</a>", "javascript:pwn()", 4) -->
<div class="review"><strong>Mallory <img src=x onerror=pwn()></strong><span class="bar" style="width:80%;"></span><div class="body">Loved it <b>a lot</b><script>pwn()</script> <a href="javascript:pwn()">x</a></div><a href="#" rel="nofollow">website</a></div>
<!-- Review("Alice", "Great <em>read</em> & fast", "https://example.org/alice?a=1&b=2", 5) -->
<div class="review"><strong>Alice</strong><span class="bar" style="width:100%;"></span><div class="body">Great <em>read</em> & fast</div><a href="https://example.org/alice?a=1&b=2" rel="nofollow">website</a></div>
The page’s pwn() function was not called by either rendering (the title still said pwn calls: 1, the single call from the deliberate unsafe innerHTML of Where XSS comes from in a GWT application).
Use the cell in a CellList<Review> or a column of a DataGrid (see Cell widgets); it is not tied to either.
The server half of the example
The server stores the review.
The sanitising on the server uses the OWASP policy of Sanitising user HTML, and a servlet or a RemoteService method applies it before saving:
public void saveReview(long bookId, Review review) {
requirePrincipal(); // authentication and authorisation first
if (review.getBody().length() > 4000) {
throw new IllegalArgumentException("Review too long");
}
String cleanBody = REVIEW_POLICY.sanitize(review.getBody());
String site = review.getWebsite();
if (!isHttps(site)) {
site = ""; // store nothing rather than a bad URL
}
repository.save(bookId, review.getReviewer(), cleanBody, site, review.getRating());
}
Four rules are in those few lines: check who is calling before anything else, bound the size, sanitise on write with a server-side allowlist, and keep only what you can show.
Escaping the reviewer name on the server is not needed because the client escapes it on output; escape at the output, where the context is known.
The client’s SimpleHtmlSanitizer and the rel="nofollow" remain as the second line of defence for reviews stored by older versions.
The other server-side parts (token-based request-forgery protection, authorisation of every RPC method, safe file uploads) are on the XSRF, RPC and server security page.
Vulnerabilities in GWT itself
Three CVEs concern XSS in GWT’s own code. All three are old; no CVE against GWT 2.6 or later was found in the National Vulnerability Database (searching for the Google Web Toolkit product) on 2026-10-08. The table gives the versions as the National Vulnerability Database and the GWT release notes state them.
| CVE | Affected | Fixed |
|---|---|---|
CVE-2012-4563 (published 2012-11-20) |
XSS in the GWT 2.4 beta and release candidate releases, and in applications built with them. The release notes say it was not in 2.3 GA. |
Partially fixed in 2.4.0 and completely fixed in the 2.5 GA release. An application built with GWT 2.4 must be rebuilt with 2.5 or later and redeployed. |
CVE-2012-5920 (published 2012-11-20) |
XSS in GWT 2.4 through 2.5 Final; the entry describes it as the result of an incomplete fix for CVE-2012-4563. |
In the 2.5.1 line. The National Vulnerability Database lists versions up to 2.5 Final as affected. The "Security Fixes" section of the 2.5.1 RC1 release notes has a single entry, the |
CVE-2013-4204 (published 2013-11-18) |
XSS in the HTML files that |
Fixed in 2.5.1 RC1. The release notes add that these files are included only if the application depends on the JUnit module, and that "despite the fix, this is not recommended". |
Three practical points:
-
The CVEs concern the toolkit’s generated and bundled code, so the fix is to rebuild with a fixed GWT, not to change your source. Nobody should be running a 2.5-era build in 2026; the table matters for inventory work on old applications.
-
Do not let the JUnit module into the deployed application.
GWTTestCasesupport belongs to the test classpath (see Build tools: Maven and Gradle);gwt-devandgwt-userare build-time dependencies, and thegwt-servletorgwt-servlet-jakartaartifact is what a WAR needs. -
The JSON data-leak problem with the identifier CVE-2007-2378, the other GWT item in this area, is a request-forgery and data-leak topic and is on the XSRF, RPC and server security page.
Outdated material in older books and posts
The 2007 book "GWT in Action" (Hanson and Tacy, Manning, first edition, GWT 1.3 and 1.4) predates every type on this page, and tutorials of that period build markup with string concatenation and feed URLs and text straight to setHTML.
Read the older material for the architecture and replace each such snippet with the safe form from this page; none of the old patterns is acceptable in a 2026 application.
Other statements that have been overtaken:
-
The Developer Guide page on SafeHtml is labelled GWT 2.1. Its API is unchanged, but it says nothing about strict CSP, the OWASP sanitiser or the fact that UiBinder now generates
SafeHtmlTemplatesby default. -
The article "Security for GWT Applications" (updated January 2009) is a good primer on the same-origin policy, XSS and request forgery; it predates
SafeUri,SafeStyles, CSP and the current Maven-based project layouts. -
Tutorials that recommend
evalfor JSON, including the deprecatedJSONParser.parse, are unsafe; useJsonUtils.safeEvalorJSONParser.parseStrict. -
The
UiBinder.useSafeHtmlTemplatesproperty in 2.3-era posts is described as off by default; it is on in 2.13.1.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
|
|
Build the whole element from constants and escaped parts, or use a |
|
A placeholder in an attribute value without quotes |
Quote the attribute value: |
|
A placeholder in a |
Do not template script; attach a handler in Java |
|
A |
Use |
|
A |
Pass the whole URL as the |
|
A |
Change the parameter type to |
|
The template uses |
Number the placeholders from |
|
A method of a |
Annotate every method |
|
A template method returns |
Return |
|
|
Fix the import |
|
|
Use the typed factories, or write |
|
|
Check for |
A link that "does nothing" and its address is |
|
Fix the data, or reject it on the server; a |
A link to another host that your site did not intend |
|
Check the host yourself when only certain hosts are acceptable |
The module does not start; console: |
A policy with a nonce but without |
Add |
Console |
The policy is doing its job; the injection point is still a bug |
Find the unsafe sink; the policy is not a fix |
Styles missing under |
|
Allow |
|
The default RPC protocol version 7 is evaluated by the client |
Start the server with |
Text after a sanitised review is bold, or an element of the layout closes early |
|
Render the sanitised body inside its own element, or sanitise on the server with a library that balances the output |
What changed recently
-
GWT 2.1: the Developer Guide labels its SafeHtml page as new in 2.1. 2.3.0: the notes warn that UiBinder’s SafeHtml integration is off by default. 2.5.0 RC1: "Added SafeHtml support to parts of the GWT user library", and
Tree.addItem(String)andinsertItem(int, String)deprecated in favour ofaddTextItemandinsertTextItem. 2.5.1 RC1: theGWTTestCaseXSS fix. 2.6.0 RC1: DOM methods and builder methods that take a URL acceptSafeUri. -
GWT 2.8.1 and 2.8.2: improvements for compliance with (strict) CSP.
-
GWT 2.12.0 (2024-10-29): CSP fixes in the linkers and development mode, and "CSP workarounds for GWT-RPC payloads"; source maps are on by default, so production deployments must decide whether to publish them (see XSRF, RPC and server security).
-
GWT 2.13.0 and 2.13.1: the release notes list no change to the safe types or to CSP behaviour.
-
OWASP Java HTML Sanitizer: the version used on this page, 20260924.2, uses date-based version numbers and is the current release on Maven Central on 2026-10-09.
Source: GWT — Release notes