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.

Table 1. Sinks that interpret a String as markup, and the safe replacement
Sink (unsafe with untrusted data) Why it is dangerous Use instead

HTML.setHTML(String), new HTML(String), InlineHTML and the other widgets that implement HasHTML

The string is assigned to innerHTML.

setHTML(SafeHtml), new HTML(SafeHtml), or setText(String) for plain text

Element.setInnerHTML(String), DOM.setInnerHTML

Same, on a raw DOM element.

Element.setInnerSafeHtml(SafeHtml) or setInnerText(String)

HTMLPanel(String), HTMLPanel(String, String)

The constructor parses the string as HTML.

HTMLPanel(SafeHtml)

Anchor(String, boolean) with asHtml true, TabPanel.add(Widget, String, boolean) with asHTML true, and similar overloads

A boolean switches the string from text to markup.

The overload that takes SafeHtml, or the String overload without the boolean

Anchor.setHref(String), AnchorElement.setHref(String), Image.setUrl(String), Frame.setUrl

A javascript: or data: URL runs script (anchor, frame) or is a request primitive for tracking (image).

The SafeUri overloads, built with UriUtils

Window.Location.assign(String), Window.Location.replace(String), Window.open(String, String, String)

The browser navigates to whatever scheme the string has.

Check UriUtils.isSafeUri(url) first, and allow only the schemes you expect (see SafeUri and UriUtils)

JSNI that writes innerHTML, calls eval or builds a Function

Native code sits outside every Java check.

Do not do it with untrusted data; see JSNI, eval and JSON as sinks

JsonUtils.unsafeEval(String) and the deprecated parse methods that evaluate their input

The text is executed as JavaScript.

JsonUtils.safeEval(String), which is JSON.parse in 2.13.1

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 by History, the token of a Place. Reflected and DOM-based XSS arrive this way.

  • The user. Text typed into a TextBox and 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 from localStorage that another page wrote.

The diagram shows the decision every value passes through before it reaches a sink.

flowchart LR src["Untrusted data
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
Figure 1. Untrusted data takes one of two paths before a sink

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.

Table 2. The safe types of GWT 2.13.1
Type Package Contract

SafeHtml

com.google.gwt.safehtml.shared

asString() can be assigned to innerHTML (or used in any "inner HTML" context) without running script, and two values can be concatenated with the same result. It extends Serializable.

SafeUri

com.google.gwt.safehtml.shared

asString() can be used as the value of a URL attribute such as href or src without running script.

SafeStyles

com.google.gwt.safecss.shared

A sequence of name:value; declarations (or the empty string) that can be used in a style attribute. It never contains angle brackets.

SafeHtmlBuilder, SafeStylesBuilder

the same packages

Accumulate pieces; text is escaped, constants are checked in development mode, other SafeHtml is appended as it is.

SafeHtmlUtils, UriUtils, SafeStylesUtils

the same packages

Static factories: escaping, sanitising and the explicit "I vouch for this string" methods.

SafeHtmlTemplates

com.google.gwt.safehtml.client

A tag interface; the compiler generates an implementation that escapes every parameter for the position where it appears.

HasSafeHtml

com.google.gwt.safehtml.client

Implemented by widgets with a setHTML(SafeHtml) method.

HtmlSanitizer, SimpleHtmlSanitizer

com.google.gwt.safehtml.shared

Turn a String that may contain simple markup into a SafeHtml.

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)           => &lt;img src=x onerror=pwn()&gt; &amp; &quot;q&quot; &#39;a&#39;
fromString(evil).asString() => &lt;img src=x onerror=pwn()&gt; &amp; &quot;q&quot; &#39;a&#39;
htmlEscapeAllowEntities("a &amp; b &lt; <c> &bogus") => a &amp; b &lt; &lt;c&gt; &amp;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 &lt;script&gt;x&lt;/script&gt;</b><br>line1<br>line&lt;2&gt;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>&lt;b onclick=x&gt;b</b></i>&#39;&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:

Table 3. How each parameter type is treated in each position
Parameter type Position Treatment

String

text content or a quoted attribute value

HTML-escaped

String

a URL attribute (href, src, …​)

UriUtils.sanitizeUri first, then HTML-escaped; the compiler logs an INFO message

SafeHtml

text content

appended as it is

SafeHtml

an attribute

compile error

SafeUri

the whole value of a URL attribute

HTML-escaped only (the value is already trusted)

SafeUri

a part of an attribute value, or text content

compile error

SafeStyles

the whole value of a style attribute

HTML-escaped

String

a style attribute

HTML-escaped, with a compile warning: the generator cannot guarantee safety in a CSS context

any

a script element or an on... handler

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&quot; onmouseover=&quot;x">link</a>
<a href="https://example.org/a?b=1&amp;c=2" title="ok">link</a>

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:

Table 4. Compile-time messages of SafeHtmlTemplatesGenerator
Template or declaration Message

<a href={0}>x</a>

Template variable in unquoted attribute value: <a href={0}

<script>var x = '{0}';</script>

Template variables in javascript context are not supported: <script>var x = '{0}

<div onclick="{0}">x</div>

Template variables in javascript context are not supported: <div onclick="{0}

<b {0}>x</b>

Template variables in tags or in attribute names are not supported: <b {0}

<a title="{0}"> with a SafeHtml parameter

SafeHtml used in a non-text context. Did you mean to use java.lang.String or SafeStyles instead?

<a href="http://x/{0}"> with a SafeUri parameter

SafeUri can only be used as the entire value of a URL attribute. Did you mean to use java.lang.String or SafeHtml instead?

<a href="{1}">{0}</a> on a one-parameter method

Argument 1 beyond range of arguments: <a href="{1}">{0}</a>

a method without @Template

Required annotation @Template not present on interface method public abstract com.google.gwt.safehtml.shared.SafeHtml m(java.lang.String s)

a method that returns String

All methods in interfaces extending SafeHtmlTemplates must have a return type of com.google.gwt.safehtml.shared.SafeHtml.

<div style="{0}">x</div> with a String parameter (a warning, not an error)

Template with variable in CSS attribute context: The template code generator cannot guarantee HTML-safety of the template -- please inspect manually or use SafeStyles to specify arguments in a CSS attribute context

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.

Table 5. The methods of UriUtils
Method Behaviour

isSafeUri(String)

true when the URI has no scheme, or the scheme is http, https, ftp or mailto (compared case-insensitively, with the Turkish-locale i handled)

sanitizeUri(String)

returns encodeAllowEscapes(uri) if the URI is safe by that rule, otherwise the string #

fromString(String)

returns a SafeUri wrapping sanitizeUri(s)

fromSafeConstant(String)

wraps a compile-time constant without checking

fromTrustedString(String)

wraps without checking; the audit point

unsafeCastFromUntrustedString(String)

deprecated; wraps without any check

extractScheme(String)

the scheme, or null

encode(String), encodeAllowEscapes(String)

percent-encode; the second leaves existing %xx escapes alone

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/x is "safe" by this definition because it has no scheme. sanitizeUri protects 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

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          => &lt;img src=x onerror=pwn()&gt; &amp; &quot;q&quot; &#39;a&#39;
ClickableTextCell => &lt;img src=x onerror=pwn()&gt; &amp; &quot;q&quot; &#39;a&#39;
SafeHtmlCell      => <b>Dune &lt;script&gt;x&lt;/script&gt;</b><br>line1<br>line&lt;2&gt;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 SafeHtmlBuilder or from a SafeHtmlTemplates interface. Never assemble markup by concatenating strings.

  • Take SafeHtml, SafeUri and SafeStyles in the signatures of your own views and presenters, so that unwrapping happens in one place.

  • Treat every use of fromTrustedString, fromSafeConstant, appendHtmlConstant, forTrusted..., trusted... and unsafeCastFromUntrustedString as 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 eval or a Function.

  • Validate URLs with UriUtils; for navigation by code check isSafeUri and 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 &amp; that is <em>good</em>, <span style=\"foo: bar\">x</span> "
    + "<b onclick=\"x()\">y</b> <script>z</script>");
foo &lt; bar &amp; that is <em>good</em>, &lt;span style=&quot;foo: bar&quot;&gt;x&lt;/span&gt; &lt;b onclick=&quot;x()&quot;&gt;y</b> &lt;script&gt;z&lt;/script&gt;

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 SimpleHtmlSanitizer or 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 SafeHtml at the very last moment, with SafeHtmlUtils.fromTrustedString only at the line that follows a sanitising call, and comment it with the name of the policy. The server’s output is a plain String; 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 with JSON.parse and throws IllegalArgumentException on 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).

Table 6. Measured results of the xsiframe lab under different policies
Policy header Module starts Injected handler ran What else happened

none

yes

yes (title changed)

ScriptInjector.fromString and fromUrl both work

script-src 'self'

yes

no

The inline onerror was blocked (script-src-attr); ScriptInjector.fromString was blocked (script-src-elem, inline); fromUrl("injected.js") worked

default-src 'self'

yes

no

Style attributes in markup (style-src-attr) and the style element that StyleInjector writes (style-src-elem) were blocked; the lab’s border-top set through a style attribute computed to 0px

default-src 'self'; style-src 'self' 'unsafe-inline'

yes

no

Styles worked again (computed 5px); only the script-src-attr violation remained

script-src 'nonce-NONCE_PLACEHOLDER' 'strict-dynamic'; object-src 'none'; base-uri 'none' with the nonce on both script tags

yes

no

ScriptInjector.fromString and fromUrl both worked

script-src 'nonce-NONCE_PLACEHOLDER' (nonce only)

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>

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"/>

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.version property: with the default protocol version 7 the client evaluates the response, which needs 'unsafe-eval'; started with -Dgwt.rpc.version=8 the server writes a payload that the client parses with JSON.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 eval users: ExternalTextResource and JsonUtils.unsafeEval need '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:9876 by 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 String in 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 style attribute.

  • The body is sanitised to a SafeHtml before it reaches the template.

  • The website goes through UriUtils.fromString, which turns a javascript: URL into #, and the link has rel="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 &lt;img src=x onerror=pwn()&gt;</strong><span class="bar" style="width:80%;"></span><div class="body">Loved it <b>a lot</b>&lt;script&gt;pwn()&lt;/script&gt; &lt;a href=&quot;javascript:pwn()&quot;&gt;x&lt;/a&gt;</div><a href="#" rel="nofollow">website</a></div>

<!-- Review("Alice", "Great <em>read</em> &amp; 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> &amp; fast</div><a href="https://example.org/alice?a=1&amp;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.

Table 7. Cross-site scripting CVEs in GWT
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 GWTTestCase files of CVE-2013-4204, so the notes do not name this CVE.

CVE-2013-4204 (published 2013-11-18)

XSS in the HTML files that GWTTestCase uses (the JUnit support), in GWT before 2.5.1 RC1.

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. GWTTestCase support belongs to the test classpath (see Build tools: Maven and Gradle); gwt-dev and gwt-user are build-time dependencies, and the gwt-servlet or gwt-servlet-jakarta artifact 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 SafeHtmlTemplates by 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 eval for JSON, including the deprecated JSONParser.parse, are unsafe; use JsonUtils.safeEval or JSONParser.parseStrict.

  • The UiBinder.useSafeHtmlTemplates property in 2.3-era posts is described as off by default; it is on in 2.13.1.

Pitfalls

Table 8. Common problems with SafeHtml, templates and CSP
Symptom Cause Fix

java.lang.AssertionError: String is not complete HTML (ends in non-inner-HTML context): <a href=' in tests (and no error in production)

appendHtmlConstant or fromSafeConstant was given a string that stops inside a tag; the check is skipped in compiled code

Build the whole element from constants and escaped parts, or use a SafeHtmlTemplates method for markup with attributes

Template variable in unquoted attribute value: <a href={0} at compile time

A placeholder in an attribute value without quotes

Quote the attribute value: href="{0}"

Template variables in javascript context are not supported: <div onclick="{0}

A placeholder in a script element or an event-handler attribute

Do not template script; attach a handler in Java

SafeHtml used in a non-text context. Did you mean to use java.lang.String or SafeStyles instead?

A SafeHtml parameter used in an attribute

Use String, SafeUri or SafeStyles for attribute positions

SafeUri can only be used as the entire value of a URL attribute. Did you mean to use java.lang.String or SafeHtml instead?

A SafeUri placeholder inside a longer attribute value, such as href="http://x/{0}"

Pass the whole URL as the SafeUri, built with UriUtils, or pass the tail as a String and let the generator escape it

Template with variable in CSS attribute context: The template code generator cannot guarantee HTML-safety of the template ... warning

A String parameter in a style attribute

Change the parameter type to SafeStyles and build it with SafeStylesUtils

Argument 1 beyond range of arguments: <a href="{1}">{0}</a>

The template uses {1} but the method has one parameter

Number the placeholders from {0} in parameter order

Required annotation @Template not present on interface method ...

A method of a SafeHtmlTemplates interface without @Template

Annotate every method

All methods in interfaces extending SafeHtmlTemplates must have a return type of com.google.gwt.safehtml.shared.SafeHtml.

A template method returns String

Return SafeHtml

cannot be resolved to a type for SafeStyles after importing from com.google.gwt.safehtml.shared

SafeStyles is in com.google.gwt.safecss.shared

Fix the import

java.lang.AssertionError: Invalid CSS Property: 'color: red'. CSS properties must be an empty string or end with a semi-colon (;). (with -ea only)

SafeStylesUtils.fromTrustedString or a trusted... method got a value that does not end with a semicolon or contains angle brackets

Use the typed factories, or write color:red;

java.lang.NullPointerException: html is null

SimpleHtmlSanitizer.sanitizeHtml(null)

Check for null before sanitising (a missing body is not an empty body)

A link that "does nothing" and its address is #

UriUtils.sanitizeUri replaced a URL whose scheme is not http, https, ftp or mailto

Fix the data, or reject it on the server; a # is working as designed

A link to another host that your site did not intend

sanitizeUri accepts scheme-relative URLs such as //evil.example/x

Check the host yourself when only certain hosts are acceptable

The module does not start; console: Loading the script '.../PERMUTATION.cache.js' violates the following Content Security Policy directive: "script-src 'nonce-...'"

A policy with a nonce but without 'self' or 'strict-dynamic'; the permutation script is loaded inside the linker’s iframe and has no nonce

Add 'strict-dynamic' (with the nonce on the bootstrap tag), or 'self'

Console Executing inline event handler violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ... or a nonce ... is required to enable inline execution. after injected markup with onerror=

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 default-src 'self'; violations for style-src-attr or style-src-elem

StyleInjector, CssResource and generated code use style elements and attributes

Allow style-src 'self' 'unsafe-inline'

EvalError: Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script on every RPC call

The default RPC protocol version 7 is evaluated by the client

Start the server with -Dgwt.rpc.version=8 (see the GWT-RPC page)

Text after a sanitised review is bold, or an element of the layout closes early

SimpleHtmlSanitizer does not guarantee balanced tags: an unbalanced closing tag such as </b> survives

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) and insertItem(int, String) deprecated in favour of addTextItem and insertTextItem. 2.5.1 RC1: the GWTTestCase XSS fix. 2.6.0 RC1: DOM methods and builder methods that take a URL accept SafeUri.

  • 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.

References