diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java b/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java index 4038fe53d21..e487a9a2456 100644 --- a/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java +++ b/wicket-core/src/main/java/org/apache/wicket/core/util/string/JavaScriptUtils.java @@ -104,6 +104,26 @@ public static CharSequence escapeQuotes(final CharSequence input) return s; } + /** + * Escape single and double quotes so that they can be part of e.g. an alert call. + * + * Note: JSON values need to escape only the double quote, so this method wont help. + * + * @param input + * the JavaScript which needs to be escaped + * @return Escaped version of the input + */ + public static CharSequence escapeQuotesAndBackslash(final CharSequence input) + { + CharSequence s = input; + if (s != null) + { + s = Strings.replaceAll(s, "\\", "\\\\"); + s = escapeQuotes(s); + } + return s; + } + /** * Write a reference to a javascript file to the response object * diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FolderUploadsFileManager.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FolderUploadsFileManager.java index 0146b820996..f877acc9a62 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FolderUploadsFileManager.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FolderUploadsFileManager.java @@ -18,7 +18,10 @@ import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.markup.html.form.upload.FileUpload; import org.apache.wicket.util.file.File; @@ -60,14 +63,62 @@ public File getFolder() { return folder; } + /** + * Returns the canonical path for a directory for use in path traversal checks. + * Uses {@code toRealPath()} when the directory exists; falls back to + * {@code toAbsolutePath().normalize()} only when the directory does not yet exist + * (e.g. an upload sub-folder that will be created during {@link #save}). + * Other {@link IOException} subtypes (e.g. permission errors) are intentionally + * re-thrown so they are not silently swallowed. + */ + private static Path getPathForComparison(java.io.File dir) throws IOException + { + try + { + return dir.toPath().toRealPath(); + } + catch (NoSuchFileException e) + { + return dir.toPath().toAbsolutePath().normalize(); + } + } + + /** + * Validates {@code uploadFieldId} and {@code clientFileName} against the base folder to + * prevent path traversal, and returns the fully resolved, canonical target file. + * + * @throws SecurityException if either component would escape the base folder + */ + private java.io.File resolveTargetFile(String uploadFieldId, String clientFileName) + throws IOException + { + Path baseFolderPath = getFolder().toPath().toRealPath(); + java.io.File uploadFieldFolder = new File(getFolder(), uploadFieldId).getCanonicalFile(); + if (!uploadFieldFolder.toPath().startsWith(baseFolderPath)) + { + throw new SecurityException("Path traversal detected in uploadFieldId"); + } + java.io.File target = new File(uploadFieldFolder, clientFileName).getCanonicalFile(); + Path uploadFieldFolderPath = getPathForComparison(uploadFieldFolder); + if (!target.toPath().startsWith(uploadFieldFolderPath)) + { + throw new SecurityException("Path traversal detected in client filename"); + } + return target; + } + @Override public void save(FileUpload fileItem, String uploadFieldId) { - File uploadFieldFolder = new File(getFolder(), uploadFieldId); - uploadFieldFolder.mkdirs(); try { - IOUtils.copy(fileItem.getInputStream(), new FileOutputStream(new File(uploadFieldFolder, fileItem.getClientFileName()))); + java.io.File target = resolveTargetFile(uploadFieldId, fileItem.getClientFileName()); + Files.createDirectories(target.toPath().getParent()); + try (InputStream in = fileItem.getInputStream(); + FileOutputStream out = new FileOutputStream(target)) + { + IOUtils.copy(in, out); + } } catch (IOException e) { @@ -78,6 +129,13 @@ public void save(FileUpload fileItem, String uploadFieldId) @Override public File getFile(String uploadFieldId, String clientFileName) { - return new File(new File(getFolder(), uploadFieldId), clientFileName); + try + { + return new File(resolveTargetFile(uploadFieldId, clientFileName)); + } + catch (IOException e) + { + throw new WicketRuntimeException(e); + } } } diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/link/ExternalLink.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/link/ExternalLink.java index 3217ee37d93..09607c8fc83 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/link/ExternalLink.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/link/ExternalLink.java @@ -16,6 +16,7 @@ */ package org.apache.wicket.markup.html.link; +import org.apache.wicket.core.util.string.JavaScriptUtils; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.OnEventHeaderItem; @@ -188,7 +189,7 @@ public void renderHead(IHeaderResponse response) { if (popupSettings != null) { - popupSettings.setTarget("'" + url + "'"); + popupSettings.setTarget(url); response.render(OnEventHeaderItem.forComponent(this, "click", popupSettings.getPopupJavaScript())); return; @@ -205,8 +206,9 @@ public void renderHead(IHeaderResponse response) // in firefox when the element is quickly clicked 3 times a second request is // generated during page load. This check ensures that the click is ignored response.render(OnEventHeaderItem.forComponent(this, "click", - "var win = this.ownerDocument.defaultView || this.ownerDocument.parentWindow; " - + "if (win == window) { window.location.href='" + url + "var win = this.ownerDocument.defaultView || this.ownerDocument.parentWindow; " // + + "if (win == window) { window.location.href='" // + + JavaScriptUtils.escapeQuotes(url) // + "'; } ;return false")); return; } diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/link/Link.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/link/Link.java index a374db164f5..872ef36147f 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/link/Link.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/link/Link.java @@ -407,7 +407,7 @@ public void renderHead(IHeaderResponse response) // next check for popup settings if (popupSettings != null) { - popupSettings.setTarget("'" + url + "'"); + popupSettings.setTarget(url.toString()); response.render(OnEventHeaderItem.forComponent(this, "click", popupSettings.getPopupJavaScript())); return; diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/link/PopupSettings.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/link/PopupSettings.java index 82dc08f82f1..e62b3e01be2 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/link/PopupSettings.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/link/PopupSettings.java @@ -16,6 +16,7 @@ */ package org.apache.wicket.markup.html.link; +import org.apache.wicket.core.util.string.JavaScriptUtils; import org.apache.wicket.util.io.IClusterable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -155,8 +156,10 @@ public String getPopupJavaScript() windowTitle = windowTitle.replaceAll("\\W", "_"); } - StringBuilder script = new StringBuilder("var w = window.open(" + target + ", '").append( - windowTitle).append("', '"); + StringBuilder script = new StringBuilder(// + "var w = window.open('"// + + JavaScriptUtils.escapeQuotes(target) // + + "', '").append(windowTitle).append("', '"); script.append("scrollbars=").append(flagToString(SCROLLBARS)); script.append(",location=").append(flagToString(LOCATION_BAR)); diff --git a/wicket-core/src/main/java/org/apache/wicket/request/resource/PackageResource.java b/wicket-core/src/main/java/org/apache/wicket/request/resource/PackageResource.java index 0cbc7b84c7d..c54fd52c327 100644 --- a/wicket-core/src/main/java/org/apache/wicket/request/resource/PackageResource.java +++ b/wicket-core/src/main/java/org/apache/wicket/request/resource/PackageResource.java @@ -16,16 +16,6 @@ */ package org.apache.wicket.request.resource; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.Locale; -import java.util.Objects; -import javax.servlet.http.HttpServletResponse; import org.apache.wicket.Application; import org.apache.wicket.IWicketInternalException; import org.apache.wicket.Session; @@ -45,7 +35,6 @@ import org.apache.wicket.util.io.IOUtils; import org.apache.wicket.util.lang.Classes; import org.apache.wicket.util.lang.Packages; -import org.apache.wicket.util.resource.IFixedLocationResourceStream; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.resource.ResourceStreamNotFoundException; import org.apache.wicket.util.resource.ResourceStreamWrapper; @@ -53,6 +42,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Locale; +import java.util.Objects; + /** * Represents a localizable static resource. *

@@ -555,38 +555,18 @@ public void setCompress(boolean compress) private IResourceStream internalGetResourceStream(final String style, final Locale locale) { + if (!accept(absolutePath)) + { + throw new PackageResourceBlockedException( + "Access denied to (static) package resource " + absolutePath + ". See IPackageResourceGuard"); + } + IResourceStreamLocator resourceStreamLocator = Application.get() .getResourceSettings() .getResourceStreamLocator(); IResourceStream resourceStream = resourceStreamLocator.locate(getScope(), absolutePath, style, variation, locale, null, false); - String realPath = absolutePath; - if (resourceStream instanceof IFixedLocationResourceStream) - { - realPath = ((IFixedLocationResourceStream)resourceStream).locationAsString(); - if (realPath != null) - { - int index = realPath.indexOf(absolutePath); - if (index != -1) - { - realPath = realPath.substring(index); - } - } - else - { - realPath = absolutePath; - } - - } - - if (accept(realPath) == false) - { - throw new PackageResourceBlockedException( - "Access denied to (static) package resource " + absolutePath + - ". See IPackageResourceGuard"); - } - if (resourceStream != null) { resourceStream = new ProcessingResourceStream(resourceStream); diff --git a/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java b/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java index b8491016152..07b5c4ec108 100644 --- a/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java +++ b/wicket-core/src/test/java/org/apache/wicket/core/util/string/JavaScriptUtilsTest.java @@ -16,10 +16,12 @@ */ package org.apache.wicket.core.util.string; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import org.apache.wicket.response.StringResponse; import org.apache.wicket.util.value.AttributeMap; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; /** @@ -97,4 +99,9 @@ public void scriptTag() JavaScriptUtils.SCRIPT_OPEN_TAG); assertEquals("\n/*]]>*/\n\n", JavaScriptUtils.SCRIPT_CLOSE_TAG); } + + @Test + void escapeQuotesAndBackslash(){ + assertThat(JavaScriptUtils.escapeQuotesAndBackslash("alert('foo\\tbar')")).isEqualTo("alert(\\'foo\\\\tbar\\')"); + } } diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/html/PackageResourceTest.java b/wicket-core/src/test/java/org/apache/wicket/markup/html/PackageResourceTest.java index 11bd855db26..0a0c00f4f04 100644 --- a/wicket-core/src/test/java/org/apache/wicket/markup/html/PackageResourceTest.java +++ b/wicket-core/src/test/java/org/apache/wicket/markup/html/PackageResourceTest.java @@ -16,16 +16,11 @@ */ package org.apache.wicket.markup.html; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Locale; - import org.apache.wicket.Application; import org.apache.wicket.SharedResources; +import org.apache.wicket.markup.html.snake_case.TestPageInsideSnakeCasePackage; import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.protocol.https.HttpPage; import org.apache.wicket.request.resource.JavaScriptPackageResource; import org.apache.wicket.request.resource.PackageResource; import org.apache.wicket.request.resource.PackageResourceReference; @@ -35,6 +30,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + /** * Tests for package resources. * @@ -193,4 +194,59 @@ void javascriptFileWithEncoding() final String contentType = tester.getLastResponse().getContentType(); assertEquals("text/javascript; charset=" + encoding, contentType); } + + @Test + void getResourceStream() + { + PackageResource resource = new PackageResourceReference(PackageResourceTest.class, + "packaged1.txt").getResource(); + assertThat(resource.getResourceStream()).isNotNull(); + } + + @Test + void dontGetResourceStream() + { + PackageResource resource = new PackageResourceReference(HttpPage.class, + "HttpPage.html").getResource(); + assertThatThrownBy(resource::getResourceStream).isInstanceOf( + PackageResource.PackageResourceBlockedException.class); + } + + @Test + void dontGetResourceStreamIfNameHasSuffix() + { + PackageResource resource = new PackageResourceReference(HttpPage.class, + "HttpPage_en.html").getResource(); + assertThatThrownBy(resource::getResourceStream).isInstanceOf( + PackageResource.PackageResourceBlockedException.class); + } + + @Test + void getResourceStreamInSnakeCasePackage() + { + PackageResource resource = new PackageResourceReference( + TestPageInsideSnakeCasePackage.class, "style.css").getResource(); + assertThat(resource.getResourceStream()).isNotNull(); + } + + @Test + void dontGetResourceStreamInSnakeCasePackage() + { + PackageResource resource = new PackageResourceReference( + TestPageInsideSnakeCasePackage.class, + "TestPageInsideSnakeCasePackage.html").getResource(); + assertThatThrownBy(resource::getResourceStream).isInstanceOf( + PackageResource.PackageResourceBlockedException.class); + } + + @Test + void dontGetResourceStreamInSnakeCasePackageIfNameHasSuffix() + { + PackageResource resource = new PackageResourceReference( + TestPageInsideSnakeCasePackage.class, + "TestPageInsideSnakeCasePackage_en.html").getResource(); + assertThatThrownBy(resource::getResourceStream).isInstanceOf( + PackageResource.PackageResourceBlockedException.class); + } + } diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FolderUploadsFileManagerTest.java b/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FolderUploadsFileManagerTest.java new file mode 100644 index 00000000000..4930aed2d71 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/resource/FolderUploadsFileManagerTest.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.markup.html.form.upload.resource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.wicket.markup.html.form.upload.FileUpload; +import org.apache.wicket.util.file.File; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class FolderUploadsFileManagerTest +{ + @TempDir + Path tempDir; + + @Test + void getFileRejectsTraversalInUploadFieldId() + { + FolderUploadsFileManager manager = new FolderUploadsFileManager(new File(tempDir.toFile())); + + assertThrows(SecurityException.class, () -> manager.getFile("../escaped", "safe.txt")); + } + + @Test + void getFileRejectsTraversalInClientFileName() + { + FolderUploadsFileManager manager = new FolderUploadsFileManager(new File(tempDir.toFile())); + + assertThrows(SecurityException.class, () -> manager.getFile("uploadField", "../../etc/passwd")); + } + + @Test + void saveRejectsTraversalInUploadFieldId() throws IOException + { + FolderUploadsFileManager manager = new FolderUploadsFileManager(new File(tempDir.toFile())); + FileUpload fileUpload = mock(FileUpload.class); + when(fileUpload.getClientFileName()).thenReturn("safe.txt"); + when(fileUpload.getInputStream()) + .thenReturn(new ByteArrayInputStream("content".getBytes(StandardCharsets.UTF_8))); + + assertThrows(SecurityException.class, () -> manager.save(fileUpload, "../escaped")); + } + + @Test + void saveRejectsTraversalInClientFileName() throws IOException + { + FolderUploadsFileManager manager = new FolderUploadsFileManager(new File(tempDir.toFile())); + FileUpload fileUpload = mock(FileUpload.class); + when(fileUpload.getClientFileName()).thenReturn("../../etc/passwd"); + when(fileUpload.getInputStream()) + .thenReturn(new ByteArrayInputStream("content".getBytes(StandardCharsets.UTF_8))); + + assertThrows(SecurityException.class, () -> manager.save(fileUpload, "uploadField")); + } + + @Test + void saveSucceedsWithValidUploadFieldIdAndClientFileName() throws IOException + { + FolderUploadsFileManager manager = new FolderUploadsFileManager(new File(tempDir.toFile())); + FileUpload fileUpload = mock(FileUpload.class); + when(fileUpload.getClientFileName()).thenReturn("safe.txt"); + when(fileUpload.getInputStream()) + .thenReturn(new ByteArrayInputStream("content".getBytes(StandardCharsets.UTF_8))); + + manager.save(fileUpload, "uploadField"); + + Path savedFile = tempDir.resolve("uploadField").resolve("safe.txt"); + assertTrue(Files.exists(savedFile)); + assertEquals("content", Files.readString(savedFile, StandardCharsets.UTF_8)); + } +} diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/html/link/ClientSideImageMapTest.java b/wicket-core/src/test/java/org/apache/wicket/markup/html/link/ClientSideImageMapTest.java index b69922f8a82..e53f30dc548 100644 --- a/wicket-core/src/test/java/org/apache/wicket/markup/html/link/ClientSideImageMapTest.java +++ b/wicket-core/src/test/java/org/apache/wicket/markup/html/link/ClientSideImageMapTest.java @@ -1,38 +1,38 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.wicket.markup.html.link; - -import java.util.Locale; - -import org.apache.wicket.util.tester.WicketTestCase; -import org.junit.jupiter.api.Test; - -/** - * @since 1.5 - */ -class ClientSideImageMapTest extends WicketTestCase -{ - /** - * @throws Exception - */ - @Test - void testRenderClientSideImageMapPage_1() throws Exception - { - tester.getSession().setLocale(Locale.US); - executeTest(ClientSideImageMapPage_1.class, "ClientSideImageMapPageExpectedResult_1.html"); - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.markup.html.link; + +import java.util.Locale; + +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Test; + +/** + * @since 1.5 + */ +class ClientSideImageMapTest extends WicketTestCase +{ + /** + * @throws Exception + */ + @Test + void testRenderClientSideImageMapPage_1() throws Exception + { + tester.getSession().setLocale(Locale.US); + executeTest(ClientSideImageMapPage_1.class, "ClientSideImageMapPageExpectedResult_1.html"); + } +} diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/html/link/ExternalLinkTest.java b/wicket-core/src/test/java/org/apache/wicket/markup/html/link/ExternalLinkTest.java index ffbcd67f506..e28f607f67d 100644 --- a/wicket-core/src/test/java/org/apache/wicket/markup/html/link/ExternalLinkTest.java +++ b/wicket-core/src/test/java/org/apache/wicket/markup/html/link/ExternalLinkTest.java @@ -1,49 +1,110 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.wicket.markup.html.link; - -import org.apache.wicket.util.tester.WicketTestCase; -import org.junit.jupiter.api.Test; - -/** - * Test ExternalLink (href="...") - * - * - */ -class ExternalLinkTest extends WicketTestCase -{ - /** - * @throws Exception - */ - @Test - void renderExternalLink_1() throws Exception - { - tester.getApplication().getMarkupSettings().setAutomaticLinking(true); - executeTest(ExternalLinkPage_1.class, "ExternalLinkPageExpectedResult_1.html"); - } - - /** - * @throws Exception - */ - @Test - void renderExternalLink_2() throws Exception - { - tester.getApplication().getMarkupSettings().setAutomaticLinking(true); - executeTest(ExternalLinkPage_2.class, "ExternalLinkPageExpectedResult_2.html"); - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.markup.html.link; + +import org.apache.wicket.MockPageWithOneComponent; +import org.apache.wicket.core.util.string.JavaScriptUtils; +import org.apache.wicket.markup.ComponentTag; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Test; + +import static org.apache.wicket.MockPageWithOneComponent.COMPONENT_ID; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test ExternalLink (href="...") + * + * + */ +class ExternalLinkTest extends WicketTestCase +{ + + @Test + void allowsJavascriptScheme() throws Exception + { + String uri = "javascript:alert(1)"; + MockPageWithOneComponent page = new MockPageWithOneComponent(); + page.add(new ExternalLink(COMPONENT_ID, uri){ + @Override + protected void onComponentTag(ComponentTag tag) + { + super.onComponentTag(tag); + tag.setName("a"); + } + }); + + tester.startPage(page); + + assertThat(tester.getLastResponseAsString()).contains(uri); + } + + @Test + void escapesJavascriptQuotes() throws Exception + { + String unescaped = "javascript:alert('foo')"; + MockPageWithOneComponent page = new MockPageWithOneComponent(); + page.add(new ExternalLink(COMPONENT_ID, unescaped)); + + tester.startPage(page); + + assertThat(tester.getLastResponseAsString()).contains("javascript:alert(\\'foo\\')"); + } + + @Test + void allowsJavascriptSchemeInPopupTarget() throws Exception + { + String uri = "javascript:alert(1)"; + MockPageWithOneComponent page = new MockPageWithOneComponent(); + page.add(new ExternalLink(COMPONENT_ID, uri)); + + tester.startPage(page); + + assertThat(tester.getLastResponseAsString()).contains(uri); + } + @Test + void escapeJavascriptQuotes() throws Exception + { + String uri = "javascript:alert('foo')"; + MockPageWithOneComponent page = new MockPageWithOneComponent(); + page.add(new ExternalLink(COMPONENT_ID, uri)); + + tester.startPage(page); + + assertThat(tester.getLastResponseAsString()).contains(JavaScriptUtils.escapeQuotes(uri)); + } + + /** + * @throws Exception + */ + @Test + void renderExternalLink_1() throws Exception + { + tester.getApplication().getMarkupSettings().setAutomaticLinking(true); + executeTest(ExternalLinkPage_1.class, "ExternalLinkPageExpectedResult_1.html"); + } + + /** + * @throws Exception + */ + @Test + void renderExternalLink_2() throws Exception + { + tester.getApplication().getMarkupSettings().setAutomaticLinking(true); + executeTest(ExternalLinkPage_2.class, "ExternalLinkPageExpectedResult_2.html"); + } + +} diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/html/link/LinkTest.java b/wicket-core/src/test/java/org/apache/wicket/markup/html/link/LinkTest.java new file mode 100644 index 00000000000..e5c432db635 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/markup/html/link/LinkTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.markup.html.link; + +import org.apache.wicket.MockPageWithLink; +import org.apache.wicket.MockPageWithOneComponent; +import org.apache.wicket.WicketRuntimeException; +import org.apache.wicket.markup.ComponentTag; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Test; + +import static org.apache.wicket.MockPageWithOneComponent.COMPONENT_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class LinkTest extends WicketTestCase +{ + + @Test + void allowsJavascriptSchemeInPopupsTarget() + { + var uri = "javascript:alert(1);"; + MockPageWithOneComponent page = new MockPageWithOneComponent(); + page.add(new PopupLink(COMPONENT_ID, uri)); + + tester.startPage(page); + + assertThat(tester.getLastResponseAsString()).contains(uri); + } + + @Test + void escapesJavascriptQuotesInPopupsTarget() + { + var uri = "javascript:alert('foo');"; + MockPageWithOneComponent page = new MockPageWithOneComponent(); + page.add(new PopupLink(COMPONENT_ID, uri)); + + tester.startPage(page); + + assertThat(tester.getLastResponseAsString()).contains("javascript:alert(\\'foo\\');"); + } + + @Test + void testWrongComponentId() + { + MockPageWithLink mockPageWithLink = new MockPageWithLink(); + Link link = new Link("linkx") + { + private static final long serialVersionUID = 1L; + + @Override + public void onClick() + { + } + + }; + + mockPageWithLink.add(link); + assertThrows(WicketRuntimeException.class, () -> tester.startPage(mockPageWithLink)); + } + + static class PopupLink extends Link + { + private final String uri; + + public PopupLink(String id, String uri) + { + super(id); + this.uri = uri; + setPopupSettings(new PopupSettings()); + } + + @Override + public void onClick() + { + } + + @Override + protected void onComponentTag(ComponentTag tag) + { + super.onComponentTag(tag); + tag.setName("a"); + } + + @Override + protected CharSequence getURL() + { + return uri; + } + } + +} \ No newline at end of file diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/html/snake_case/TestPageInsideSnakeCasePackage.html b/wicket-core/src/test/java/org/apache/wicket/markup/html/snake_case/TestPageInsideSnakeCasePackage.html new file mode 100644 index 00000000000..ea75f280732 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/markup/html/snake_case/TestPageInsideSnakeCasePackage.html @@ -0,0 +1,11 @@ + + + + + + Test Page + + +none + + diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/html/snake_case/TestPageInsideSnakeCasePackage.java b/wicket-core/src/test/java/org/apache/wicket/markup/html/snake_case/TestPageInsideSnakeCasePackage.java new file mode 100644 index 00000000000..ee3288eaa81 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/markup/html/snake_case/TestPageInsideSnakeCasePackage.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.markup.html.snake_case; + +import org.apache.wicket.markup.html.WebPage; + +public class TestPageInsideSnakeCasePackage extends WebPage +{ +} diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/html/snake_case/style.css b/wicket-core/src/test/java/org/apache/wicket/markup/html/snake_case/style.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/AbstractAutoCompleteRenderer.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/AbstractAutoCompleteRenderer.java index 51186a54604..b6755ab1e97 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/AbstractAutoCompleteRenderer.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/AbstractAutoCompleteRenderer.java @@ -16,6 +16,7 @@ */ package org.apache.wicket.extensions.ajax.markup.html.autocomplete; +import org.apache.wicket.core.util.string.JavaScriptUtils; import org.apache.wicket.request.Response; import org.apache.wicket.util.string.Strings; @@ -50,7 +51,7 @@ public void render(final T object, final Response response, final String criteri final CharSequence handler = getOnSelectJavaScriptExpression(object); if (handler != null) { - response.write(" onselect=\"" + handler + '"'); + response.write(" onselect=\"" + Strings.escapeMarkup(handler) + '"'); } response.write(">"); renderChoice(object, response, criteria); diff --git a/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/AbstractAutoCompleteRendererTest.java b/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/AbstractAutoCompleteRendererTest.java new file mode 100644 index 00000000000..59e80ab6513 --- /dev/null +++ b/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/AbstractAutoCompleteRendererTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.extensions.ajax.markup.html.autocomplete; + +import org.apache.wicket.mock.MockWebResponse; +import org.apache.wicket.request.Response; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class AbstractAutoCompleteRendererTest extends WicketTestCase +{ + @Test + void escapeOnselectJSExpression() + { + var renderer = new Renderer("alert('hello');"); + MockWebResponse response = new MockWebResponse(); + renderer.render("foo", response, null); + Assertions.assertTrue(response.getTextResponse().toString() + .contains("

  • foo
  • ")); + } + + static class Renderer extends AbstractAutoCompleteRenderer + { + + final String expression; + + Renderer(String expression) + { + this.expression = expression; + } + + @Override + protected CharSequence getOnSelectJavaScriptExpression(String item) + { + return expression; + } + + @Override + protected void renderChoice(String object, Response response, String criteria) + { + response.write(object); + } + + @Override + protected String getTextValue(String object) + { + return object; + } + } +}