VYPR
Moderate severityNVD Advisory· Published Apr 23, 2021· Updated Sep 17, 2024

Directory traversal in development mode handler in Vaadin 14 and 15-17

CVE-2020-36321

Description

Improper URL validation in development mode handler in com.vaadin:flow-server versions 2.0.0 through 2.4.1 (Vaadin 14.0.0 through 14.4.2), and 3.0 prior to 5.0 (Vaadin 15 prior to 18) allows attacker to request arbitrary files stored outside of intended frontend resources folder.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
com.vaadin:flow-serverMaven
>= 3.0.0, < 5.0.05.0.0
com.vaadin:flow-serverMaven
>= 2.0.0, < 2.4.22.4.2

Affected products

2
  • Range: 14.0.0
  • Vaadin/flow-serverv5
    Range: 2.0.0

Patches

5
a5bc6b4832e6

18.0.0

https://github.com/vaadin/vaadinVaadin BotNov 30, 2020via osv
1 file changed · +1 1
  • package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@vaadin/vaadin",
    -  "version": "18.0.0-rc2",
    +  "version": "18.0.0",
       "description": "Vaadin components is an evolving set of open sourced custom HTML elements for building mobile and desktop web applications in modern browsers.",
       "author": "Vaadin Ltd",
       "license": "(Apache-2.0 OR SEE LICENSE IN https://vaadin.com/license/cval-3.0)",
    
60b4fd8e5994

fix: optimize handling of requests containing Range header (#9484)

https://github.com/vaadin/flowJohannes ErikssonNov 25, 2020via osv
2 files changed · +103 6
  • flow-server/src/main/java/com/vaadin/flow/internal/ResponseWriter.java+54 6 modified
    @@ -29,8 +29,8 @@
     import java.net.MalformedURLException;
     import java.net.URL;
     import java.net.URLConnection;
    -import java.util.ArrayList;
     import java.util.List;
    +import java.util.Stack;
     import java.util.UUID;
     import java.util.regex.Matcher;
     import java.util.regex.Pattern;
    @@ -51,8 +51,20 @@
     public class ResponseWriter implements Serializable {
         private static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
     
    -    private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("^bytes=(([0-9]*-[0-9]*,?\\s*)+)$");
    -    private static final Pattern BYTE_RANGE_PATTERN = Pattern.compile("([0-9]*)-([0-9]*)");
    +    private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile(
    +            "^bytes=((\\d*-\\d*\\s*,\\s*)*\\d*-\\d*\\s*)$");
    +    private static final Pattern BYTE_RANGE_PATTERN = Pattern.compile(
    +            "(\\d*)-(\\d*)");
    +
    +    /**
    +     * Maximum number of ranges accepted in a single Range header. Remaining ranges will be ignored.
    +     */
    +    private static final int MAX_RANGE_COUNT = 16;
    +
    +    /**
    +     * Maximum number of overlapping ranges allowed. The request will be denied if above this threshold.
    +     */
    +    private static final int MAX_OVERLAPPING_RANGE_COUNT = 2;
     
         private final int bufferSize;
         private final boolean brotliEnabled;
    @@ -196,13 +208,14 @@ private void writeRangeContents(String range, HttpServletResponse response,
             long resourceLength = connection.getContentLengthLong();
             Matcher rangeMatcher = BYTE_RANGE_PATTERN.matcher(byteRanges);
     
    -        List<Pair<Long, Long>> ranges = new ArrayList<>();
    -        while (rangeMatcher.find()) {
    +        Stack<Pair<Long, Long>> ranges = new Stack<>();
    +        while (rangeMatcher.find() && ranges.size() < MAX_RANGE_COUNT) {
                 String startGroup = rangeMatcher.group(1);
                 String endGroup = rangeMatcher.group(2);
                 if (startGroup.isEmpty() && endGroup.isEmpty()) {
                     response.setContentLengthLong(0L);
                     response.setStatus(416); // Range Not Satisfiable
    +                getLogger().info("received a malformed range: '{}'", rangeMatcher.group());
                     return;
                 }
                 long start = startGroup.isEmpty() ? 0L : Long.parseLong(startGroup);
    @@ -211,11 +224,20 @@ private void writeRangeContents(String range, HttpServletResponse response,
                 if (end < start
                         || (resourceLength >= 0 && start >= resourceLength)) {
                     // illegal range -> 416
    +                getLogger().info("received an illegal range '{}' for resource '{}'",
    +                        rangeMatcher.group(), resourceURL);
                     response.setContentLengthLong(0L);
                     response.setStatus(416);
                     return;
                 }
    -            ranges.add(new Pair<>(start, end));
    +            ranges.push(new Pair<>(start, end));
    +
    +            if (!verifyRangeLimits(ranges)) {
    +                ranges.pop();
    +                getLogger().info("serving only {} ranges for resource '{}' even though more were requested",
    +                        ranges.size(), resourceURL);
    +                break;
    +            }
             }
     
             response.setStatus(206);
    @@ -314,6 +336,32 @@ private void setContentLength(HttpServletResponse response,
             }
         }
     
    +    /**
    +     * Returns true if the number of ranges in <code>ranges</code> is less than the
    +     * upper limit and the number that overlap (= have at least one byte in common)
    +     * with the range <code>[start, end]</code> are less than the upper limit.
    +     */
    +    private boolean verifyRangeLimits(List<Pair<Long, Long>> ranges) {
    +        if (ranges.size() > MAX_RANGE_COUNT) {
    +            getLogger().info("more than {} ranges requested", MAX_RANGE_COUNT);
    +            return false;
    +        }
    +        int count = 0;
    +        for (int i = 0; i < ranges.size(); i++) {
    +            for (int j = i + 1; j < ranges.size(); j++) {
    +                if (ranges.get(i).getFirst() <= ranges.get(j).getSecond()
    +                        && ranges.get(j).getFirst() <= ranges.get(i).getSecond()) {
    +                    count++;
    +                }
    +            }
    +        }
    +        if (count > MAX_OVERLAPPING_RANGE_COUNT) {
    +            getLogger().info("more than {} overlapping ranges requested", MAX_OVERLAPPING_RANGE_COUNT);
    +            return false;
    +        }
    +        return true;
    +    }
    +
         private URL getResource(HttpServletRequest request, String resource)
                 throws MalformedURLException {
             URL url = request.getServletContext().getResource(resource);
    
  • flow-server/src/test/java/com/vaadin/flow/internal/ResponseWriterTest.java+49 0 modified
    @@ -486,6 +486,44 @@ public void writeByteRangeMultiPartOverlapping() throws IOException {
             assertStatus(206);
         }
     
    +    @Test
    +    public void writeByteRangeMultiPartTooManyRequested() throws IOException {
    +        makePathsAvailable(PATH_JS);
    +        mockRequestHeaders(new Pair<>("Range", "bytes=0-0, 0-0, 1-1, 2-2, 3-3, 4-4, 5-5, 6-6, 7-7, 8-8, 9-9, 10-10, 11-11, 12-12, 13-13, 14-14, 15-15, 16-16"));
    +        // "File.js contents"
    +        // ^0123456789ABCDEF^
    +        assertMultipartResponse(PATH_JS, Arrays.asList(
    +                new Pair<>(new String[]{"Content-Range: bytes 0-0/16"}, "F".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 0-0/16"}, "F".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 1-1/16"}, "i".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 2-2/16"}, "l".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 3-3/16"}, "e".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 4-4/16"}, ".".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 5-5/16"}, "j".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 6-6/16"}, "s".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 7-7/16"}, " ".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 8-8/16"}, "c".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 9-9/16"}, "o".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 10-10/16"}, "n".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 11-11/16"}, "t".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 12-12/16"}, "e".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 13-13/16"}, "n".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 14-14/16"}, "t".getBytes())));
    +        assertStatus(206);
    +    }
    +
    +    @Test
    +    public void writeByteRangeMultiPartTooManyOverlappingRequested() throws IOException {
    +        makePathsAvailable(PATH_JS);
    +        mockRequestHeaders(new Pair<>("Range", "bytes=2-4, 0-4, 3-14"));
    +        // "File.js contents"
    +        // ^0123456789ABCDEF^
    +        assertMultipartResponse(PATH_JS, Arrays.asList(
    +                new Pair<>(new String[]{"Content-Range: bytes 2-4/16"}, "le.".getBytes()),
    +                new Pair<>(new String[]{"Content-Range: bytes 0-4/16"}, "File.".getBytes())));
    +        assertStatus(206);
    +    }
    +
         private void assertResponse(byte[] expectedResponse) throws IOException {
             assertResponse(PATH_JS, expectedResponse);
         }
    @@ -542,6 +580,17 @@ private void assertMultipartResponse(String path,
                 byte[] bytes = outputStream.toByteArray();
                 Assert.assertArrayEquals(expectedBytes, bytes);
             }
    +
    +        // check that there are no excess parts
    +        try {
    +            mps.readHeaders();
    +            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    +            mps.readBodyData(outputStream);
    +            Assert.assertTrue("excess bytes in multipart response",
    +                    outputStream.toByteArray().length == 0);
    +        } catch (IOException ioe) {
    +            // all is well, stream ended
    +        }
         }
     
         private void assertStatus(int status) {
    
e9b8eba22d38

14.4.3

https://github.com/vaadin/vaadinVaadin BotNov 24, 2020via osv
2 files changed · +2 2
  • bower.json+1 1 modified
    @@ -24,7 +24,7 @@
         "vaadin-charts": "vaadin-charts#6.3.1",
         "vaadin-confirm-dialog": "vaadin-confirm-dialog#1.3.0",
         "vaadin-cookie-consent": "vaadin-cookie-consent#1.2.0",
    -    "vaadin-core": "vaadin-core#14.4.3",
    +    "vaadin-core": "14.4.3",
         "vaadin-crud": "vaadin-crud#1.3.0",
         "vaadin-grid-pro": "vaadin-grid-pro#2.2.2",
         "vaadin-license-checker": "vaadin-license-checker#2.1.2",
    
  • package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@vaadin/vaadin",
    -  "version": "14.4.2",
    +  "version": "14.4.3",
       "description": "Vaadin components is an evolving set of open sourced custom HTML elements for building mobile and desktop web applications in modern browsers.",
       "author": "Vaadin Ltd",
       "license": "(Apache-2.0 OR SEE LICENSE IN https://vaadin.com/license/cval-3.0)",
    
4623398d1c68

chore: update header

https://github.com/vaadin/flowMikhail ShabarovNov 18, 2020via osv
1 file changed · +2 1
  • flow-server/src/main/java/com/vaadin/flow/server/HandlerHelper.java+2 1 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2000-2018 Vaadin Ltd.
    + * Copyright 2000-2020 Vaadin Ltd.
      *
      * Licensed 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
    @@ -13,6 +13,7 @@
      * License for the specific language governing permissions and limitations under
      * the License.
      */
    +
     package com.vaadin.flow.server;
     
     import java.io.Serializable;
    
19fe3864f9d1

fix: check if Url contains directory change in Dev Mode (#9392)

https://github.com/vaadin/flowMikhail ShabarovNov 18, 2020via osv
6 files changed · +198 24
  • flow-server/src/main/java/com/vaadin/flow/server/DevModeHandler.java+7 0 modified
    @@ -305,6 +305,13 @@ public boolean serveDevModeRequest(HttpServletRequest request,
             // a valid request for webpack-dev-server should start with '/VAADIN/'
             String requestFilename = request.getPathInfo();
     
    +        if (HandlerHelper.isPathUnsafe(requestFilename)) {
    +            getLogger().info(HandlerHelper.UNSAFE_PATH_ERROR_MESSAGE_PATTERN,
    +                    requestFilename);
    +            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
    +            return true;
    +        }
    +
             HttpURLConnection connection = prepareConnection(requestFilename,
                     request.getMethod());
     
    
  • flow-server/src/main/java/com/vaadin/flow/server/HandlerHelper.java+35 0 modified
    @@ -16,8 +16,12 @@
     package com.vaadin.flow.server;
     
     import java.io.Serializable;
    +import java.io.UnsupportedEncodingException;
    +import java.net.URLDecoder;
    +import java.nio.charset.StandardCharsets;
     import java.util.Locale;
     import java.util.function.BiConsumer;
    +import java.util.regex.Pattern;
     
     import com.vaadin.flow.component.UI;
     import com.vaadin.flow.shared.ApplicationConstants;
    @@ -35,6 +39,15 @@ public class HandlerHelper implements Serializable {
          */
         static final SystemMessages DEFAULT_SYSTEM_MESSAGES = new SystemMessages();
     
    +    /**
    +     * The pattern of error message shown when the URL path contains unsafe
    +     * double encoding.
    +     */
    +    static final String UNSAFE_PATH_ERROR_MESSAGE_PATTERN = "Blocked attempt to access file: {}";
    +
    +    private static final Pattern PARENT_DIRECTORY_REGEX = Pattern
    +            .compile("(/|\\\\)\\.\\.(/|\\\\)?", Pattern.CASE_INSENSITIVE);
    +
         /**
          * Framework internal enum for tracking the type of a request.
          */
    @@ -176,4 +189,26 @@ public static String getCancelingRelativePath(String pathToCancel) {
             return sb.toString();
         }
     
    +    /**
    +     * Checks if the given URL path contains the directory change instruction
    +     * (dot-dot), taking into account possible double encoding in hexadecimal
    +     * format, which can be injected maliciously.
    +     *
    +     * @param path
    +     *            the URL path to be verified.
    +     * @return {@code true}, if the given path has a directory change
    +     *         instruction, {@code false} otherwise.
    +     */
    +    public static boolean isPathUnsafe(String path) {
    +        // Check that the path does not have '/../', '\..\', %5C..%5C,
    +        // %2F..%2F, nor '/..', '\..', %5C.., %2F..
    +        try {
    +            path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
    +        } catch (UnsupportedEncodingException e) {
    +            throw new RuntimeException("An error occurred during decoding URL.",
    +                    e);
    +        }
    +        return PARENT_DIRECTORY_REGEX.matcher(path).find();
    +    }
    +
     }
    
  • flow-server/src/main/java/com/vaadin/flow/server/StaticFileServer.java+3 20 modified
    @@ -20,11 +20,8 @@
     
     import java.io.IOException;
     import java.io.InputStream;
    -import java.io.UnsupportedEncodingException;
     import java.net.URL;
     import java.net.URLConnection;
    -import java.net.URLDecoder;
    -import java.nio.charset.StandardCharsets;
     import java.util.regex.Pattern;
     
     import org.slf4j.Logger;
    @@ -55,8 +52,6 @@ public class StaticFileServer implements StaticFileHandler {
                 + "fixIncorrectWebjarPaths";
         private static final Pattern INCORRECT_WEBJAR_PATH_REGEX = Pattern
                 .compile("^/frontend[-\\w/]*/webjars/");
    -    private static final Pattern PARENT_DIRECTORY_REGEX = Pattern
    -            .compile("(/|\\\\)\\.\\.(/|\\\\)", Pattern.CASE_INSENSITIVE);
     
         private final ResponseWriter responseWriter;
         private final VaadinServletService servletService;
    @@ -108,10 +103,10 @@ public boolean serveStaticResource(HttpServletRequest request,
                 HttpServletResponse response) throws IOException {
     
             String filenameWithPath = getRequestFilename(request);
    -        if (!isPathSafe(filenameWithPath)) {
    -            getLogger().info("Blocked attempt to access file: {}",
    +        if (HandlerHelper.isPathUnsafe(filenameWithPath)) {
    +            getLogger().info(HandlerHelper.UNSAFE_PATH_ERROR_MESSAGE_PATTERN,
                         filenameWithPath);
    -            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
    +            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                 return true;
             }
     
    @@ -190,18 +185,6 @@ private String fixIncorrectWebjarPath(String requestFilename) {
                     .replaceAll("/webjars/");
         }
     
    -    private boolean isPathSafe(String path) {
    -        // Check that the path does not have '/../', '\..\', %5C..%5C, or
    -        // %2F..%2F
    -        try {
    -            path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
    -        } catch (UnsupportedEncodingException e) {
    -            throw new RuntimeException("An error occurred during decoding URL.",
    -                    e);
    -        }
    -        return !PARENT_DIRECTORY_REGEX.matcher(path).find();
    -    }
    -
         /**
          * Check if it is ok to serve the requested file from the classpath.
          * <p>
    
  • flow-server/src/test/java/com/vaadin/flow/server/DevModeHandlerTest.java+69 0 modified
    @@ -63,6 +63,7 @@
     import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_DEVMODE_WEBPACK_TIMEOUT;
     import static com.vaadin.flow.server.frontend.NodeUpdateTestUtil.WEBPACK_TEST_OUT_FILE;
     import static com.vaadin.flow.server.frontend.NodeUpdateTestUtil.createStubWebpackServer;
    +import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
     import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
     import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
     import static java.net.HttpURLConnection.HTTP_OK;
    @@ -580,6 +581,74 @@ public void devModeNotReady_handleRequest_returnsHtml() throws Exception {
             Mockito.verify(response).setContentType("text/html;charset=utf-8");
         }
     
    +    @Test
    +    public void serveDevModeRequest_uriWithDirectoryChangeWithSlash_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        verifyServeDevModeRequestReturnsTrueAndSetsProperStatusCode(
    +                "/VAADIN/build/../vaadin-bundle-1234.cache.js");
    +    }
    +
    +    @Test
    +    public void serveDevModeRequest_uriWithDirectoryChangeWithBackslash_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        verifyServeDevModeRequestReturnsTrueAndSetsProperStatusCode(
    +                "/VAADIN/build/something\\..\\vaadin-bundle-1234.cache.js");
    +    }
    +
    +    @Test
    +    public void serveDevModeRequest_uriWithDirectoryChangeWithEncodedBackslashUpperCase_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        verifyServeDevModeRequestReturnsTrueAndSetsProperStatusCode(
    +                "/VAADIN/build/something%5C..%5Cvaadin-bundle-1234.cache.js");
    +    }
    +
    +    @Test
    +    public void serveDevModeRequest_uriWithDirectoryChangeWithEncodedBackslashLowerCase_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        verifyServeDevModeRequestReturnsTrueAndSetsProperStatusCode(
    +                "/VAADIN/build/something%5c..%5cvaadin-bundle-1234.cache.js");
    +    }
    +
    +    @Test
    +    public void serveDevModeRequest_uriWithDirectoryChangeInTheEndWithSlash_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        verifyServeDevModeRequestReturnsTrueAndSetsProperStatusCode(
    +                "/VAADIN/build/..");
    +    }
    +
    +    @Test
    +    public void serveDevModeRequest_uriWithDirectoryChangeInTheEndWithBackslash_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        verifyServeDevModeRequestReturnsTrueAndSetsProperStatusCode(
    +                "/VAADIN/build/something\\..");
    +    }
    +
    +    @Test
    +    public void serveDevModeRequest_uriWithDirectoryChangeInTheEndWithEncodedBackslashUpperCase_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        verifyServeDevModeRequestReturnsTrueAndSetsProperStatusCode(
    +                "/VAADIN/build/something%5C..");
    +    }
    +
    +    @Test
    +    public void serveDevModeRequest_uriWithDirectoryChangeInTheEndWithEncodedBackslashLowerCase_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        verifyServeDevModeRequestReturnsTrueAndSetsProperStatusCode(
    +                "/VAADIN/build/something%5c..");
    +    }
    +
    +    private void verifyServeDevModeRequestReturnsTrueAndSetsProperStatusCode(
    +            String uri) throws IOException {
    +        HttpServletRequest request = prepareRequest(uri);
    +        HttpServletResponse response = prepareResponse();
    +        DevModeHandler handler = DevModeHandler.start(configuration, npmFolder,
    +                CompletableFuture.completedFuture(null));
    +        handler.join();
    +        assertTrue(handler.serveDevModeRequest(request, response));
    +
    +        Assert.assertEquals(HTTP_FORBIDDEN, responseStatus);
    +    }
    +
         private VaadinServlet prepareServlet(int port)
                 throws ServletException, IOException {
             DevModeHandler.start(port, configuration, npmFolder,
    
  • flow-server/src/test/java/com/vaadin/flow/server/StaticFileServerTest.java+34 4 modified
    @@ -537,36 +537,66 @@ private void staticBuildResourceWithDirectoryChange_nothingServed(
     
             Assert.assertTrue(fileServer.serveStaticResource(request, response));
             Assert.assertEquals(0, out.getOutput().length);
    +        Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN,
    +                responseCode.get());
         }
     
         @Test
    -    public void staticBuildResourceWithDirectoryChangeWithSlash_nothingServed()
    +    public void serveStaticResource_uriWithDirectoryChangeWithSlash_returnsImmediatelyAndSetsForbiddenStatus()
                 throws IOException {
             staticBuildResourceWithDirectoryChange_nothingServed(
                     "/VAADIN/build/../vaadin-bundle-1234.cache.js");
         }
     
         @Test
    -    public void staticBuildResourceWithDirectoryChangeWithBackslash_nothingServed()
    +    public void serveStaticResource_uriWithDirectoryChangeWithBackslash_returnsImmediatelyAndSetsForbiddenStatus()
                 throws IOException {
             staticBuildResourceWithDirectoryChange_nothingServed(
                     "/VAADIN/build/something\\..\\vaadin-bundle-1234.cache.js");
         }
     
         @Test
    -    public void staticBuildResourceWithDirectoryChangeWithEncodedBackslashUpperCase_nothingServed()
    +    public void serveStaticResource_uriWithDirectoryChangeWithEncodedBackslashUpperCase_returnsImmediatelyAndSetsForbiddenStatus()
                 throws IOException {
             staticBuildResourceWithDirectoryChange_nothingServed(
                     "/VAADIN/build/something%5C..%5Cvaadin-bundle-1234.cache.js");
         }
     
         @Test
    -    public void staticBuildResourceWithDirectoryChangeWithEncodedBackslashLowerCase_nothingServed()
    +    public void serveStaticResource_uriWithDirectoryChangeWithEncodedBackslashLowerCase_returnsImmediatelyAndSetsForbiddenStatus()
                 throws IOException {
             staticBuildResourceWithDirectoryChange_nothingServed(
                     "/VAADIN/build/something%5c..%5cvaadin-bundle-1234.cache.js");
         }
     
    +    @Test
    +    public void serveStaticResource_uriWithDirectoryChangeInTheEndWithSlash_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        staticBuildResourceWithDirectoryChange_nothingServed(
    +                "/VAADIN/build/..");
    +    }
    +
    +    @Test
    +    public void serveStaticResource_uriWithDirectoryChangeInTheEndWithBackslash_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        staticBuildResourceWithDirectoryChange_nothingServed(
    +                "/VAADIN/build/something\\..");
    +    }
    +
    +    @Test
    +    public void serveStaticResource_uriWithDirectoryChangeInTheEndWithEncodedBackslashUpperCase_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        staticBuildResourceWithDirectoryChange_nothingServed(
    +                "/VAADIN/build/something%5C..");
    +    }
    +
    +    @Test
    +    public void serveStaticResource_uriWithDirectoryChangeInTheEndWithEncodedBackslashLowerCase_returnsImmediatelyAndSetsForbiddenStatus()
    +            throws IOException {
    +        staticBuildResourceWithDirectoryChange_nothingServed(
    +                "/VAADIN/build/something%5c..");
    +    }
    +
         @Test
         public void customStaticBuildResource_isServed() throws IOException {
             String pathInfo = "/VAADIN/build/my-text.txt";
    
  • flow-tests/test-dev-mode/src/test/java/com/vaadin/flow/uitest/ui/UrlValidationIT.java+50 0 added
    @@ -0,0 +1,50 @@
    +/*
    + * Copyright 2000-2020 Vaadin Ltd.
    + *
    + * Licensed 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 com.vaadin.flow.uitest.ui;
    +
    +import java.io.IOException;
    +import java.net.HttpURLConnection;
    +import java.net.URL;
    +
    +import com.vaadin.flow.testutil.ChromeBrowserTest;
    +import org.junit.Assert;
    +import org.junit.Test;
    +
    +public class UrlValidationIT extends ChromeBrowserTest {
    +
    +    @Test
    +    public void devModeUriValidation_uriWithDirectoryChange_statusForbidden() throws IOException {
    +        sendRequestAndValidateResponseStatusForbidden(
    +                "/VAADIN/build/%252E%252E/");
    +    }
    +
    +    @Test
    +    public void staticResourceUriValidation_uriWithDirectoryChange_statusForbidden() throws IOException {
    +        sendRequestAndValidateResponseStatusForbidden(
    +                "/VAADIN/build/%252E%252E/some-resource.css");
    +    }
    +
    +    private void sendRequestAndValidateResponseStatusForbidden(String pathToResource) throws IOException {
    +        final String urlString = getRootURL() + "/view" + pathToResource;
    +        URL url = new URL(urlString);
    +        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    +        connection.setRequestMethod("GET");
    +        int responseCode = connection.getResponseCode();
    +        Assert.assertEquals("HTTP 403 Forbidden expected for urls with " +
    +                "directory change", HttpURLConnection.HTTP_FORBIDDEN, responseCode);
    +    }
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.