Skip to content

Commit 2c8d6b2

Browse files
committed
CSP follow-ups: error event for blocked objects (FF) and for links with a blocked @import;
document.open() removes all event listeners; HTMLObjectElement.data
1 parent e15bf6a commit 2c8d6b2

16 files changed

Lines changed: 281 additions & 35 deletions

‎src/changes/changes.xml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88

99
<body>
1010
<release version="5.8.0" date="xxxx xx, 2026" description="Chrome/Edge 157, Bugfixes">
11+
<action type="add" dev="rbri">
12+
HTMLObjectElement.data implemented.
13+
</action>
14+
<action type="fix" dev="rbri">
15+
document.open() removes all event listeners and handlers of the document, its nodes and the window.
16+
</action>
17+
<action type="fix" dev="rbri">
18+
Content-Security-Policy: the error event is fired at a link if an @import of the stylesheet is blocked;
19+
when simulating Firefox, the error event is fired at blocked object elements.
20+
</action>
1121
<action type="add" dev="rbri">
1222
Content-Security-Policy: the nonce property of html and svg elements is supported; like the browsers,
1323
the nonce attribute is hidden if the page has a policy delivered by a header and elements

‎src/main/java/org/htmlunit/BrowserVersionFeatures.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public enum BrowserVersionFeatures {
5252
@BrowserFeature({FF, FF_ESR})
5353
CSP_NONCE_HIDING_ENFORCED_ONLY,
5454

55+
/** The error event is fired at an object element if the data url is blocked by the object-src directive. */
56+
@BrowserFeature({FF, FF_ESR})
57+
CSP_OBJECT_BLOCKED_FIRES_ERROR,
58+
5559
/**
5660
* The Content-Security-Policy violation report (report-uri) uses the Firefox format:
5761
* the keys are sorted, the report uris of the original policy are absolute, the script-sample

‎src/main/java/org/htmlunit/css/CssStyleSheet.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ public class CssStyleSheet implements Serializable {
161161
/** The CSS import rules and their corresponding stylesheets. */
162162
private final Map<CSSImportRuleImpl, CssStyleSheet> imports_ = new HashMap<>();
163163

164+
/** True if this (imported) style sheet was not loaded because of the Content-Security-Policy. */
165+
private boolean blockedByContentSecurityPolicy_;
166+
164167
/** cache parsed media strings. */
165168
private static final Map<String, MediaListImpl> MEDIA = Collections.synchronizedMap(new HashMap<>(100));
166169

@@ -1270,6 +1273,7 @@ public CssStyleSheet getImportedStyleSheet(final CSSImportRuleImpl importRule) {
12701273
if (owner_ != null && owner_.getPage() instanceof HtmlPage htmlPage
12711274
&& !ContentSecurityPolicy.allowsExternalStyle(htmlPage, null, UrlUtils.toUrlSafe(url))) {
12721275
sheet = new CssStyleSheet(owner_, "", url);
1276+
sheet.blockedByContentSecurityPolicy_ = true;
12731277
}
12741278
else {
12751279
sheet = loadStylesheet(owner_, null, url);
@@ -1279,6 +1283,32 @@ public CssStyleSheet getImportedStyleSheet(final CSSImportRuleImpl importRule) {
12791283
return sheet;
12801284
}
12811285

1286+
/**
1287+
* Loads all imported style sheets (including nested imports) like the browsers do before
1288+
* the load event of the link element is fired.
1289+
*
1290+
* @return {@code false} if at least one import was blocked by the Content-Security-Policy
1291+
*/
1292+
public boolean loadImports() {
1293+
return loadImports(new HashSet<>());
1294+
}
1295+
1296+
private boolean loadImports(final Set<String> alreadyProcessing) {
1297+
boolean allowed = true;
1298+
for (final AbstractCSSRuleImpl rule : wrapped_.getCssRules().getRules()) {
1299+
if (rule instanceof CSSImportRuleImpl importRule) {
1300+
final CssStyleSheet sheet = getImportedStyleSheet(importRule);
1301+
if (sheet.blockedByContentSecurityPolicy_) {
1302+
allowed = false;
1303+
}
1304+
else if (alreadyProcessing.add(sheet.getUri()) && !sheet.loadImports(alreadyProcessing)) {
1305+
allowed = false;
1306+
}
1307+
}
1308+
}
1309+
return allowed;
1310+
}
1311+
12821312
/**
12831313
* Returns {@code true} if this stylesheet is active, based on the media types it is associated with (if any).
12841314
* @return {@code true} if this stylesheet is active, based on the media types it is associated with (if any)

‎src/main/java/org/htmlunit/html/DomNode.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,17 @@ public DomNode cloneNode(final boolean deep) {
873873
return newnode;
874874
}
875875

876+
/**
877+
* <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
878+
* Returns the JavaScript object that corresponds to this node if already created; in contrast to
879+
* {@link #getScriptableObject()} no new object is created.
880+
*
881+
* @return the JavaScript object or {@code null}
882+
*/
883+
public HtmlUnitScriptable getScriptableObjectIfCreated() {
884+
return scriptObject_;
885+
}
886+
876887
/**
877888
* <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
878889
*

‎src/main/java/org/htmlunit/html/HtmlLink.java‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,17 @@ public CssStyleSheet getSheet() {
384384

385385
// fire the load/error event after the stylesheet is stored; otherwise
386386
// an event handler accessing the styles would load the stylesheet again
387-
final String event = pendingStylesheetEvent_;
387+
String event = pendingStylesheetEvent_;
388388
pendingStylesheetEvent_ = null;
389+
390+
// like the browsers, the error event is fired if an @import is blocked by the
391+
// Content-Security-Policy (the imports are loaded before the load event)
392+
if (Event.TYPE_LOAD.equals(event) && sheet_ != null
393+
&& getPage() instanceof HtmlPage htmlPage && htmlPage.getContentSecurityPolicy() != null
394+
&& !sheet_.loadImports()) {
395+
event = Event.TYPE_ERROR;
396+
}
397+
389398
if (event != null) {
390399
executeEvent(getPage().getWebClient(), event);
391400
}

‎src/main/java/org/htmlunit/html/HtmlObject.java‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@
1414
*/
1515
package org.htmlunit.html;
1616

17+
import static org.htmlunit.BrowserVersionFeatures.CSP_OBJECT_BLOCKED_FIRES_ERROR;
18+
1719
import java.util.Map;
1820

1921
import org.apache.commons.logging.Log;
2022
import org.apache.commons.logging.LogFactory;
2123
import org.htmlunit.SgmlPage;
24+
import org.htmlunit.WebClient;
25+
import org.htmlunit.javascript.PostponedAction;
26+
import org.htmlunit.javascript.host.event.Event;
2227
import org.htmlunit.javascript.host.html.HTMLObjectElement;
2328
import org.htmlunit.util.StringUtils;
2429
import org.htmlunit.xml.XmlPage;
@@ -265,7 +270,7 @@ public final String getVspaceAttribute() {
265270
@Override
266271
protected void onAddedToPage() {
267272
super.onAddedToPage();
268-
ContentSecurityPolicy.allowsObject(this, getAttributeDirect("data"));
273+
checkContentSecurityPolicy(getAttributeDirect("data"));
269274
}
270275

271276
/**
@@ -283,7 +288,40 @@ protected void setAttributeNS(final String namespaceURI, final String qualifiedN
283288
notifyAttributeChangeListeners, notifyMutationObservers);
284289

285290
if (changed && isAttachedToPage()) {
286-
ContentSecurityPolicy.allowsObject(this, value);
291+
checkContentSecurityPolicy(value);
292+
}
293+
}
294+
295+
/**
296+
* Checks the url against the Content-Security-Policy (object-src); if the url is blocked,
297+
* Firefox fires the error event (async).
298+
*/
299+
private void checkContentSecurityPolicy(final String data) {
300+
if (ContentSecurityPolicy.allowsObject(this, data)) {
301+
return;
302+
}
303+
304+
final HtmlPage htmlPage = getHtmlPageOrNull();
305+
if (htmlPage == null || !hasFeature(CSP_OBJECT_BLOCKED_FIRES_ERROR)) {
306+
return;
307+
}
308+
309+
final WebClient client = htmlPage.getWebClient();
310+
if (!client.isJavaScriptEngineEnabled()) {
311+
return;
312+
}
313+
314+
final PostponedAction action = new PostponedAction(htmlPage, "HtmlObject.blocked") {
315+
@Override
316+
public void execute() {
317+
HtmlObject.this.fireEvent(Event.TYPE_ERROR);
318+
}
319+
};
320+
if (READY_STATE_LOADING.equals(htmlPage.getReadyState())) {
321+
htmlPage.addAfterLoadAction(action);
322+
}
323+
else {
324+
client.getJavaScriptEngine().addPostponedAction(action);
287325
}
288326
}
289327

‎src/main/java/org/htmlunit/javascript/host/event/EventTarget.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ public final EventListenersContainer getEventListenersContainer() {
8282
return eventListenersContainer_;
8383
}
8484

85+
/**
86+
* <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
87+
* Removes all event listeners and event handlers of this event target
88+
* (e.g. done by document.open()).
89+
*/
90+
public void removeAllEventListeners() {
91+
eventListenersContainer_ = null;
92+
}
93+
8594
/**
8695
* Executes the event on this object only (needed for instance for {@code onload} on {@code (i)frame} tags).
8796
*

‎src/main/java/org/htmlunit/javascript/host/html/HTMLDocument.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import org.htmlunit.javascript.host.dom.NodeList;
6464
import org.htmlunit.javascript.host.dom.Selection;
6565
import org.htmlunit.javascript.host.event.Event;
66+
import org.htmlunit.javascript.host.event.EventTarget;
6667
import org.htmlunit.util.UrlUtils;
6768

6869
/**
@@ -470,6 +471,17 @@ public HTMLDocument open(final Object url, final Object name, final Object featu
470471
LOG.warn("Function open() called when document is already open.");
471472
}
472473
writeInCurrentDocument_ = false;
474+
475+
// like the browsers, document.open() removes all event listeners and handlers
476+
// of the document, all its nodes and the window
477+
for (final DomNode node : page.getDescendants()) {
478+
if (node.getScriptableObjectIfCreated() instanceof EventTarget target) {
479+
target.removeAllEventListeners();
480+
}
481+
}
482+
removeAllEventListeners();
483+
getWindow().removeAllEventListeners();
484+
473485
final WebWindow ww = getWindow().getWebWindow();
474486
if (ww instanceof FrameWindow window
475487
&& UrlUtils.ABOUT_BLANK.equals(getPage().getUrl().toExternalForm())) {

‎src/main/java/org/htmlunit/javascript/host/html/HTMLObjectElement.java‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
*/
1515
package org.htmlunit.javascript.host.html;
1616

17+
import java.net.MalformedURLException;
18+
1719
import org.htmlunit.html.DomElement;
1820
import org.htmlunit.html.HtmlForm;
1921
import org.htmlunit.html.HtmlObject;
22+
import org.htmlunit.html.HtmlPage;
2023
import org.htmlunit.javascript.configuration.JsxClass;
2124
import org.htmlunit.javascript.configuration.JsxConstructor;
2225
import org.htmlunit.javascript.configuration.JsxFunction;
@@ -44,6 +47,39 @@ public void jsConstructor() {
4447
super.jsConstructor();
4548
}
4649

50+
/**
51+
* Returns the {@code data} property; the url of the data attribute resolved against the
52+
* base url of the page (an empty attribute is resolved to the base url, a missing attribute
53+
* returns an empty string).
54+
* @return the {@code data} property
55+
*/
56+
@JsxGetter
57+
public String getData() {
58+
final DomElement element = getDomNodeOrDie();
59+
final String data = element.getAttributeDirect("data");
60+
if (DomElement.ATTRIBUTE_NOT_DEFINED == data) {
61+
return "";
62+
}
63+
if (element.getPage() instanceof HtmlPage page) {
64+
try {
65+
return page.getFullyQualifiedUrl(data).toExternalForm();
66+
}
67+
catch (final MalformedURLException e) {
68+
return data;
69+
}
70+
}
71+
return data;
72+
}
73+
74+
/**
75+
* Sets the {@code data} attribute.
76+
* @param data the {@code data} attribute value
77+
*/
78+
@JsxSetter
79+
public void setData(final String data) {
80+
getDomNodeOrDie().setAttribute("data", data);
81+
}
82+
4783
/**
4884
* Gets the {@code border} attribute.
4985
* @return the {@code border} attribute

‎src/site/xdoc/csp.xml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@
9898
<td>
9999
inline <code>&lt;style&gt;</code> elements (parser inserted and created by JavaScript),
100100
stylesheets loaded by <code>&lt;link rel="stylesheet"&gt;</code> (the <code>error</code>
101-
event is fired for blocked stylesheets) and <code>@import</code> rules; blocked
101+
event is fired for blocked stylesheets) and <code>@import</code> rules (the
102+
<code>error</code> event is fired at the link if an import is blocked); blocked
102103
styles are not applied
103104
</td>
104105
</tr>
@@ -148,7 +149,8 @@
148149
<code>&lt;embed&gt;</code> elements (also if added by script or if the attribute is
149150
changed later). HtmlUnit does not load the content of these elements; the url is
150151
checked anyway to report the violation (cross origin urls are reported as origin).
151-
There are no <code>load</code> and <code>error</code> events.
152+
There is no <code>load</code> event; when simulating Firefox the <code>error</code>
153+
event is fired at blocked <code>&lt;object&gt;</code> elements.
152154
</td>
153155
</tr>
154156
<tr>

0 commit comments

Comments
 (0)