Skip to content

Commit 2f438a9

Browse files
mcollovatiplatosha
andauthored
feat: validate URL schemes in Anchor, IFrame and Page#open (CP: 24.10) (#24938)
This PR cherry-picks changes from the original PRs #24539 and #24897 to branch 25.1, and also disables the validation by default. The defaults when "safeUrlSchemes" configuration is missing is adjusted, so that URL validation is disabled by default, and all URLs are considered safe. This avoids unexpected behavior changes for existing users of Flow 25.1 and below. The URL scheme validation feature could be enabled with configuration when necessary. (cherry picked from commit 28e55a3) (cherry picked from commit 51048d1) Co-authored-by: Anton Platonov <anton@vaadin.com>
1 parent d04dbaa commit 2f438a9

17 files changed

Lines changed: 771 additions & 1 deletion

File tree

flow-html-components/src/main/java/com/vaadin/flow/component/html/Anchor.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
import com.vaadin.flow.component.PropertyDescriptor;
2020
import com.vaadin.flow.component.PropertyDescriptors;
2121
import com.vaadin.flow.component.Tag;
22+
import com.vaadin.flow.function.DeploymentConfiguration;
23+
import com.vaadin.flow.internal.UrlUtil;
2224
import com.vaadin.flow.server.AbstractStreamResource;
25+
import com.vaadin.flow.server.InitParameters;
2326
import com.vaadin.flow.server.streams.AbstractDownloadHandler;
2427
import com.vaadin.flow.server.streams.DownloadHandler;
2528
import com.vaadin.flow.server.StreamResource;
@@ -62,6 +65,13 @@ public Anchor() {
6265
* the href to set
6366
* @param text
6467
* the text content to set
68+
* @throws IllegalArgumentException
69+
* if {@code href} uses a scheme that is not considered safe
70+
* according to
71+
* {@link DeploymentConfiguration#getUrlSafeSchemes()}; see
72+
* {@link #setUnsafeHref(String)} and the
73+
* {@value InitParameters#URL_SAFE_SCHEMES} configuration
74+
* property
6575
*/
6676
public Anchor(String href, String text) {
6777
setHref(href);
@@ -81,6 +91,13 @@ public Anchor(String href, String text) {
8191
* the text content to set
8292
* @param target
8393
* the target window, tab or frame
94+
* @throws IllegalArgumentException
95+
* if {@code href} uses a scheme that is not considered safe
96+
* according to
97+
* {@link DeploymentConfiguration#getUrlSafeSchemes()}; see
98+
* {@link #setUnsafeHref(String)} and the
99+
* {@value InitParameters#URL_SAFE_SCHEMES} configuration
100+
* property
84101
* @since 8.0
85102
*/
86103
public Anchor(String href, String text, AnchorTarget target) {
@@ -179,6 +196,13 @@ public Anchor(DownloadHandler downloadHandler,
179196
* the href to set
180197
* @param components
181198
* the components to add
199+
* @throws IllegalArgumentException
200+
* if {@code href} uses a scheme that is not considered safe
201+
* according to
202+
* {@link DeploymentConfiguration#getUrlSafeSchemes()}; see
203+
* {@link #setUnsafeHref(String)} and the
204+
* {@value InitParameters#URL_SAFE_SCHEMES} configuration
205+
* property
182206
* @since 1.3
183207
*/
184208
public Anchor(String href, Component... components) {
@@ -200,8 +224,42 @@ public Anchor(String href, Component... components) {
200224
*
201225
* @param href
202226
* the href to set
227+
* @throws IllegalArgumentException
228+
* if the URL uses a scheme that is not considered safe
229+
* according to
230+
* {@link DeploymentConfiguration#getUrlSafeSchemes()}; see
231+
* {@link #setUnsafeHref(String)} and the
232+
* {@value InitParameters#URL_SAFE_SCHEMES} configuration
233+
* property
203234
*/
204235
public void setHref(String href) {
236+
if (href == null) {
237+
throw new IllegalArgumentException("Href must not be null");
238+
}
239+
if (!UrlUtil.isSafeUrl(href)) {
240+
throw new IllegalArgumentException(UrlUtil.getUnsafeUrlMessage(
241+
"href", href, "setUnsafeHref(String)"));
242+
}
243+
this.href = href;
244+
assignHrefAttribute();
245+
}
246+
247+
/**
248+
* Sets the URL that this anchor links to without validating its scheme.
249+
* <p>
250+
* Unlike {@link #setHref(String)}, this method does not reject URLs based
251+
* on the {@value InitParameters#URL_SAFE_SCHEMES} configuration. Use it
252+
* only for URLs that are fully under your control and known to be safe,
253+
* such as a hard-coded {@code javascript:} or {@code data:} URL. Passing
254+
* untrusted input here can expose the application to cross-site scripting
255+
* (XSS) attacks.
256+
*
257+
* @see #setHref(String)
258+
*
259+
* @param href
260+
* the href to set
261+
*/
262+
public void setUnsafeHref(String href) {
205263
if (href == null) {
206264
throw new IllegalArgumentException("Href must not be null");
207265
}

flow-html-components/src/main/java/com/vaadin/flow/component/html/IFrame.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
import com.vaadin.flow.component.PropertyDescriptor;
1414
import com.vaadin.flow.component.PropertyDescriptors;
1515
import com.vaadin.flow.component.Tag;
16+
import com.vaadin.flow.function.DeploymentConfiguration;
17+
import com.vaadin.flow.internal.UrlUtil;
1618
import com.vaadin.flow.server.AbstractStreamResource;
19+
import com.vaadin.flow.server.InitParameters;
1720
import com.vaadin.flow.server.streams.AbstractDownloadHandler;
1821
import com.vaadin.flow.server.streams.DownloadHandler;
1922
import com.vaadin.flow.server.StreamResource;
@@ -125,6 +128,13 @@ public IFrame() {
125128
*
126129
* @param src
127130
* Source URL
131+
* @throws IllegalArgumentException
132+
* if {@code src} uses a scheme that is not considered safe
133+
* according to
134+
* {@link DeploymentConfiguration#getUrlSafeSchemes()}; see
135+
* {@link #setUnsafeSrc(String)} and the
136+
* {@value InitParameters#URL_SAFE_SCHEMES} configuration
137+
* property
128138
*/
129139
public IFrame(String src) {
130140
setSrc(src);
@@ -155,8 +165,38 @@ public IFrame(DownloadHandler downloadHandler) {
155165
*
156166
* @param src
157167
* Source URL.
168+
* @throws IllegalArgumentException
169+
* if the URL uses a scheme that is not considered safe
170+
* according to
171+
* {@link DeploymentConfiguration#getUrlSafeSchemes()}; see
172+
* {@link #setUnsafeSrc(String)} and the
173+
* {@value InitParameters#URL_SAFE_SCHEMES} configuration
174+
* property
158175
*/
159176
public void setSrc(String src) {
177+
if (src != null && !UrlUtil.isSafeUrl(src)) {
178+
throw new IllegalArgumentException(UrlUtil
179+
.getUnsafeUrlMessage("src", src, "setUnsafeSrc(String)"));
180+
}
181+
set(srcDescriptor, src);
182+
}
183+
184+
/**
185+
* Sets the source of the iframe without validating its scheme.
186+
* <p>
187+
* Unlike {@link #setSrc(String)}, this method does not reject URLs based on
188+
* the {@value InitParameters#URL_SAFE_SCHEMES} configuration. Use it only
189+
* for URLs that are fully under your control and known to be safe, such as
190+
* a hard-coded {@code javascript:} or {@code data:} URL. Passing untrusted
191+
* input here can expose the application to cross-site scripting (XSS)
192+
* attacks.
193+
*
194+
* @see #setSrc(String)
195+
*
196+
* @param src
197+
* Source URL.
198+
*/
199+
public void setUnsafeSrc(String src) {
160200
set(srcDescriptor, src);
161201
}
162202

flow-html-components/src/main/java/com/vaadin/flow/component/html/Image.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.vaadin.flow.component.PropertyDescriptors;
1818
import com.vaadin.flow.component.Tag;
1919
import com.vaadin.flow.server.AbstractStreamResource;
20+
import com.vaadin.flow.server.InitParameters;
2021
import com.vaadin.flow.server.streams.AbstractDownloadHandler;
2122
import com.vaadin.flow.server.streams.DownloadHandler;
2223
import com.vaadin.flow.server.StreamResource;
@@ -121,6 +122,10 @@ public String getSrc() {
121122

122123
/**
123124
* Sets the image URL.
125+
* <p>
126+
* Unlike {@link Anchor#setHref(String)} and {@link IFrame#setSrc(String)},
127+
* image URLs are not validated against the
128+
* {@value InitParameters#URL_SAFE_SCHEMES} configuration.
124129
*
125130
* @param src
126131
* the image URL

flow-html-components/src/test/java/com/vaadin/flow/component/html/AnchorTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,40 @@ public void customDownloadHandler_nullType_constructorSetsDownloadMode() {
425425
anchor.isDownload());
426426
}
427427

428+
@Test
429+
public void setHref_unsafeScheme_throws() {
430+
Anchor anchor = new Anchor();
431+
Assert.assertThrows(IllegalArgumentException.class,
432+
() -> anchor.setHref("javascript:alert(1)"));
433+
}
434+
435+
@Test
436+
public void setUnsafeHref_unsafeScheme_setsHrefWithoutValidation() {
437+
Anchor anchor = new Anchor();
438+
anchor.setUnsafeHref("javascript:alert(1)");
439+
Assert.assertEquals("javascript:alert(1)",
440+
anchor.getElement().getAttribute("href"));
441+
}
442+
443+
@Test
444+
public void constructor_stringHrefStringText_unsafeScheme_throws() {
445+
Assert.assertThrows(IllegalArgumentException.class,
446+
() -> new Anchor("javascript:alert(1)", "Click"));
447+
}
448+
449+
@Test
450+
public void constructor_stringHrefStringTextTarget_unsafeScheme_throws() {
451+
Assert.assertThrows(IllegalArgumentException.class,
452+
() -> new Anchor("javascript:alert(1)", "Click",
453+
AnchorTarget.BLANK));
454+
}
455+
456+
@Test
457+
public void constructor_stringHrefComponents_unsafeScheme_throws() {
458+
Assert.assertThrows(IllegalArgumentException.class,
459+
() -> new Anchor("javascript:alert(1)"));
460+
}
461+
428462
private void mockUI() {
429463
ui = new UI();
430464
UI.setCurrent(ui);

flow-html-components/src/test/java/com/vaadin/flow/component/html/ComponentTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,21 @@
2222
import java.util.Set;
2323
import java.util.stream.Stream;
2424

25+
import org.junit.AfterClass;
2526
import org.junit.Assert;
2627
import org.junit.Before;
28+
import org.junit.BeforeClass;
2729
import org.junit.Test;
30+
import org.mockito.MockedStatic;
31+
import org.mockito.Mockito;
2832

2933
import com.vaadin.flow.component.Component;
3034
import com.vaadin.flow.component.HasAriaLabel;
3135
import com.vaadin.flow.component.HasOrderedComponents;
3236
import com.vaadin.flow.component.HtmlComponent;
3337
import com.vaadin.flow.dom.Element;
38+
import com.vaadin.flow.function.DeploymentConfiguration;
39+
import com.vaadin.flow.server.VaadinService;
3440

3541
public abstract class ComponentTest {
3642

@@ -39,6 +45,28 @@ public abstract class ComponentTest {
3945

4046
private Set<String> WHITE_LIST = new HashSet<>();
4147

48+
private static MockedStatic<VaadinService> vaadinServiceMockedStatic;
49+
50+
@BeforeClass
51+
public static void init() {
52+
// Mock strict Flow 25.2+ safe URL scheme validation configuration
53+
// for feature validation in component tests (AnchorTest, IFrameTest)
54+
DeploymentConfiguration config = Mockito
55+
.mock(DeploymentConfiguration.class);
56+
Mockito.when(config.getUrlSafeSchemes())
57+
.thenReturn(Set.of("http", "https", "mailto", "tel", "ftp"));
58+
VaadinService service = Mockito.mock(VaadinService.class);
59+
Mockito.when(service.getDeploymentConfiguration()).thenReturn(config);
60+
vaadinServiceMockedStatic = Mockito.mockStatic(VaadinService.class);
61+
vaadinServiceMockedStatic.when(VaadinService::getCurrent)
62+
.thenReturn(service);
63+
}
64+
65+
@AfterClass
66+
public static void clean() {
67+
vaadinServiceMockedStatic.close();
68+
}
69+
4270
@Before
4371
public void setup() throws IntrospectionException, InstantiationException,
4472
IllegalAccessException, ClassNotFoundException {

flow-html-components/src/test/java/com/vaadin/flow/component/html/HtmlComponentSmokeTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ private static boolean isSpecialSetter(Method method) {
247247
return true;
248248
}
249249

250+
if (method.getDeclaringClass() == IFrame.class
251+
&& method.getName().equals("setUnsafeSrc")) {
252+
return true;
253+
}
254+
250255
if (method.getDeclaringClass() == HtmlObject.class
251256
&& method.getName().startsWith("setData")
252257
&& method.getParameterTypes()[0] == DownloadHandler.class) {
@@ -259,6 +264,11 @@ private static boolean isSpecialSetter(Method method) {
259264
return true;
260265
}
261266

267+
if (method.getDeclaringClass() == Anchor.class
268+
&& method.getName().equals("setUnsafeHref")) {
269+
return true;
270+
}
271+
262272
if (method.getDeclaringClass() == Image.class
263273
&& method.getName().startsWith("setSrc")
264274
&& method.getParameterTypes()[0] == DownloadHandler.class) {

flow-html-components/src/test/java/com/vaadin/flow/component/html/IFrameTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,24 @@ public Element getElement() {
106106
new TestIFrame(handler);
107107
Assert.assertTrue(handler.isInline());
108108
}
109+
110+
@Test
111+
public void setSrc_unsafeScheme_throws() {
112+
IFrame iframe = new IFrame();
113+
Assert.assertThrows(IllegalArgumentException.class,
114+
() -> iframe.setSrc("javascript:alert(1)"));
115+
}
116+
117+
@Test
118+
public void setUnsafeSrc_unsafeScheme_setsSrcWithoutValidation() {
119+
IFrame iframe = new IFrame();
120+
iframe.setUnsafeSrc("javascript:alert(1)");
121+
Assert.assertEquals("javascript:alert(1)", iframe.getSrc());
122+
}
123+
124+
@Test
125+
public void constructor_unsafeSrc_throws() {
126+
Assert.assertThrows(IllegalArgumentException.class,
127+
() -> new IFrame("javascript:alert(1)"));
128+
}
109129
}

0 commit comments

Comments
 (0)