Skip to content

Anchor and IFrame: validate URL schemes against the application's own configuration, re-checking on attach - #25185

Open
totally-not-ai[bot] wants to merge 4 commits into
mainfrom
fix/25086-validate-url-schemes-on-attach
Open

Anchor and IFrame: validate URL schemes against the application's own configuration, re-checking on attach#25185
totally-not-ai[bot] wants to merge 4 commits into
mainfrom
fix/25086-validate-url-schemes-on-attach

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #25086.

UrlUtil.isSafeUrl(String) resolved the allowed URL schemes from VaadinService.getCurrent(), which is only available while a session is locked on the current thread. A component tree built in a background thread and attached to the UI later was therefore validated against the framework defaults rather than the application's own vaadin.urlSafeSchemes configuration — and in versions where that default is a wildcard, validation was silently skipped altogether. The same fallback could also reject a URL whose scheme the application had explicitly allowed.

What changed

Validation is now tied to the component's own UI. Anchor and IFrame resolve the safe schemes from the VaadinService of the UI they are attached to. There is no fallback to VaadinService.getCurrent() or to the framework defaults, since either can differ from the configuration of the application the component ends up in.

Unvalidated URLs are re-checked on attach. If a URL is set while the component isn't attached, a one-shot attach listener is registered. Attach is the point where the correct configuration is guaranteed to be known, and also the point where the value would first be sent to the browser. The value is cleared before the exception is thrown, so an unsafe URL is not sent to the client even if the application catches the exception. Methods that replace the value without validating it (setUnsafeHref, removeHref, the stream-resource and DownloadHandler setters, …) cancel any pending check.

As a behavioral consequence, setting a URL with an unsafe scheme on a detached component now throws when the component is attached rather than immediately. The Javadoc of the setters and constructors describes this.

Bookkeeping moved into UrlUtil. Anchor and IFrame had duplicated structure for validating a URL, scheduling and canceling the attach-time re-check, and building the error message. UrlUtil.validateUrl(...) now handles all of it, and UrlUtil.cancelUrlValidation(...) serves the methods that bypass validation. The pending registration is stored as component data, so the components need neither a field nor the related null handling.

Page#open uses the UI it belongs to instead of the thread local.

UrlUtil.isSafeUrl(String) is deprecated because it relies on the current thread having a locked session, and it now logs at warning level when it falls back to the default schemes — nothing will re-check the URL in that case. The component-aware paths stay silent when a check on attach is scheduled instead.

ComponentEventBus: removing a component event listener is now idempotent regardless of whether it happens through the returned Registration or through ComponentEvent#unregisterListener, since both use the same registration instance. This removes the need to track elsewhere whether a self-removing listener has already fired.

Tests

AnchorTest and IFrameTest build their UI from MockUI and the shared mock service and session instead of hand-written mocks, and cover setting URLs while detached, the deferred throw on attach, and cancellation. UrlUtilTest exercises validateUrl and the component-aware overload directly from flow-server, since the previous coverage came only from flow-html-components and did not count toward the flow-server quality gate. PageTest covers Page#open resolving the configuration from its own UI.

API Changes

com.vaadin.flow.internal.UrlUtil

// Added
public static void validateUrl(Component component, String type, String url, String unsafeMethod) // for components whose value cannot be re-checked later, such as a UI
public static void validateUrl(Component component, String type, String url, String unsafeMethod, SerializableRunnable urlClearer) // defers the check to attach time when the configuration isn't known yet
public static void cancelUrlValidation(Component component, String type) // cancels a deferred check

// Changed
- public static boolean isSafeUrl(String url)
+ @Deprecated(since = "25.3") public static boolean isSafeUrl(String url) // relies on the current thread having a locked session; now logs a warning when falling back to the default schemes

UrlUtil.isSafeUrl relied on VaadinService.getCurrent(), which is only
defined while a session is locked. A component tree that is created in a
background thread and only attached to the UI later on was therefore
validated against the framework default instead of the application's own
safeUrlSchemes configuration. In the versions where the default is a
wildcard, this silently skipped validation altogether.

Anchor and IFrame now register a one-shot attach listener when a URL is
set while the component isn't attached. Attaching is the point where the
right configuration is guaranteed to be known, and also the point where
the value would first be sent to the browser. The value is cleared before
the exception is thrown, so an unsafe URL isn't sent to the client even if
the application catches the exception. Methods that replace the value
without validating it cancel the pending check.

The check at set time now resolves the configuration from the component's
own UI when it is attached, and Page#open uses the UI it belongs to, so
neither depends on the thread local when a better source is available.
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Test Results

 1 450 files  ± 0   1 451 suites  ±0   1h 27m 18s ⏱️ + 1m 30s
10 424 tests +30  10 357 ✅ +30  67 💤 ±0  0 ❌ ±0 
10 860 runs  +30  10 792 ✅ +30  68 💤 ±0  0 ❌ ±0 

Results for commit 5960e27. ± Comparison against base commit a1e3b17.

♻️ This comment has been updated with latest results.

The Sonar quality gate reported 67% coverage on new code because the
validateUrlOnAttach logic in UrlUtil was only exercised through the
Anchor and IFrame tests in flow-html-components, which don't contribute
to the coverage report of flow-server. UrlUtilTest now tests the helper
and the component-aware isSafeUrl overload directly, and IFrameTest
covers attaching an already validated src and the deprecated stream
resource setter.

Take the URL to validate as a value instead of a supplier, since every
method that changes the value either schedules a new check or cancels the
pending one. This also removes the redundant isDebugEnabled guard around
a parameterized log call and a src null check in IFrame that cannot be
reached, as setting a null src fails before the check is scheduled.
Comment thread flow-html-components/src/main/java/com/vaadin/flow/component/html/Anchor.java Outdated
Comment thread flow-html-components/src/test/java/com/vaadin/flow/component/html/AnchorTest.java Outdated
Comment thread flow-server/src/main/java/com/vaadin/flow/component/page/Page.java Outdated
Comment thread flow-server/src/main/java/com/vaadin/flow/internal/UrlUtil.java
Comment thread flow-server/src/main/java/com/vaadin/flow/internal/UrlUtil.java Outdated
Comment thread flow-server/src/main/java/com/vaadin/flow/internal/UrlUtil.java Outdated
Anchor and IFrame had the same structure for validating a URL and for
scheduling and canceling the check that is repeated on attach, and each
of them repeated the strings used for the error message. UrlUtil now
offers validateUrl, which checks the URL against the best available
configuration, throws with a consistent message and schedules the check
to be repeated on attach when needed, and cancelUrlValidation for the
methods that replace the value without validating it. The pending
registration is stored as component data, so the components no longer
need a field and the related null handling.

Removing a component event listener is now idempotent regardless of
whether it happens through the returned registration or through
ComponentEvent#unregisterListener, as both now use the same registration
instance. This removes the need for tracking elsewhere whether a
self-removing listener has already been fired.

Deprecate the URL check that relies on the current thread having a
locked session, and log at warning level when it falls back to the
default schemes, since nothing will re-check the URL in that case. The
component-aware checks stay silent when a check on attach is scheduled.

The tests now build their UI from MockUI and the shared mock service and
session instead of hand-written mocks.
@totally-not-ai totally-not-ai Bot changed the title fix: re-check URL schemes when a component is attached (#25086) I'll run the api-changes skill and look at the diff in parallel.Anchor and IFrame now re-validate URL schemes on attach Aug 13, 2026
@github-actions github-actions Bot added +0.1.0 and removed +0.0.1 labels Aug 13, 2026
@Legioth Legioth changed the title I'll run the api-changes skill and look at the diff in parallel.Anchor and IFrame now re-validate URL schemes on attach fix: re-check URL schemes when a component is attached (#25086) Aug 13, 2026
*/
public static void validateUrl(Component component, String type, String url,
String unsafeMethod, SerializableRunnable urlClearer) {
validateUrl(component, type, url, unsafeMethod);

@Legioth Legioth Aug 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this means that there's a possibility of falling back to the defaults if the component is not attached and there's no current service. That would lead to redundant errors in case the application's own configuration is more lenient than the default configuration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and that would have been a real regression risk. The fallback to the defaults is gone: a component that is not attached is no longer checked at all, and the check happens when it is attached, which is also the point where the value would first be sent to the browser. So a scheme the application has explicitly allowed is never rejected.

// the application wasn't available, so it has to be repeated once the
// component knows which application it belongs to
cancelUrlValidation(component, type);
if (component.getUI().isEmpty()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant if validation succeeded based on the current service. There is a very hypothetical case of configuring a detached component under one current service and then attaching the component to the UI of another service.

I wonder if the appropriate approach would be to never check the current service in any other variant than the old deprecated one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with that: the current service is now only consulted by the deprecated isSafeUrl(String). The component variants read the configuration solely from the component own UI, so there is nothing redundant left - either the component is attached and the check happens right away, or it is not and the check is deferred to attach. That also removes the wrong-application case you describe. The visible trade-off is that setting an unsafe URL on a detached component now throws when it is attached rather than immediately; the setter Javadoc says so, and the constructors point at the setter.

// Unregister before validating so that the same value isn't
// reported again if the application catches the exception and
// attaches the component another time
event.unregisterListener();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicit unregister seems redundant since the following line of code will also do the same as a side effect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. cancelUrlValidation on the next line removes the listener through the stored registration, and that is idempotent, so the explicit unregister added nothing.

private static VaadinService findService(Component component) {
VaadinService service = component.getUI().map(UI::getSession)
.map(VaadinSession::getService).orElse(null);
if (service == null) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed if the above orElse is replaced with orElseGet. Though on the other hand, I called the fallback to getCurrent() into question in another comment so maybe the if should just be removed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if is gone along with the getCurrent() fallback. The lookup is now just component.getUI().map(UI::getSession).map(VaadinSession::getService).map(...getUrlSafeSchemes()) returning an Optional, and an empty result is what makes the caller defer the check to attach instead of guessing.

AtomicBoolean cleared = new AtomicBoolean();
UrlUtil.validateUrlOnAttach(component, "href", "setUnsafeHref(String)",
"http://example.com", () -> cleared.set(true));
// Safe according to the framework default, so this doesn't throw yet

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want the opposite. If the application's own configuration is more lenient, then this shouldn't fail due to the framework default either.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - that test asserted the wrong thing and is replaced. There are now tests that a detached component is not checked against the framework defaults (custom:foo passes and stays when the application allows custom) and that a current service is not used for it either. The component tests that expected an immediate throw for javascript: on a detached component now expect it on attach instead.

The check no longer falls back to VaadinService.getCurrent() or to the
framework defaults. Both of those can differ from the configuration of
the application that the component ends up in, which means that a URL
using a scheme that the application has explicitly allowed could be
rejected, and that a check based on the current service was either
redundant with the check on attach or based on the wrong application.

A component that isn't attached is therefore not checked at all until it
is attached, which is also the point where the value would first be sent
to the browser. As a consequence, setting a URL with an unsafe scheme on
a detached component throws when the component is attached rather than
immediately, which the Javadoc of the setters now describes.

Also drop the explicit unregister in the deferred check, as canceling
the validation on the next line removes the listener anyway, and let the
service lookup be a plain Optional now that there is no fallback.
@totally-not-ai totally-not-ai Bot changed the title fix: re-check URL schemes when a component is attached (#25086) Anchor and IFrame: validate URL schemes against the application's own configuration, re-checking on attach Aug 13, 2026
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: 🔎Iteration reviews

Development

Successfully merging this pull request may close these issues.

Log a more easily noticeable warning if safeUrlSchemes isn't available

2 participants