src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java
branchhttp-client-branch
changeset 56132 c8a1eccbc719
parent 56092 fd85b2bf2b0d
child 56138 4f92b988600e
--- a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java	Thu Feb 15 12:31:14 2018 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java	Thu Feb 15 14:10:27 2018 +0000
@@ -26,11 +26,13 @@
 package jdk.internal.net.http;
 
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.net.URI;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.security.AccessControlContext;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentMap;
 import java.util.function.Function;
@@ -39,8 +41,10 @@
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandler;
 import java.net.http.HttpResponse.BodySubscriber;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import jdk.internal.net.http.ResponseSubscribers.PathSubscriber;
-import static jdk.internal.net.http.common.Utils.unchecked;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
 
 public final class ResponseBodyHandlers {
 
@@ -124,7 +128,7 @@
     // Similar to Path body handler, but for file download. Supports setting ACC.
     public static class FileDownloadBodyHandler implements UntrustedBodyHandler<Path> {
         private final Path directory;
-        private final OpenOption[]openOptions;
+        private final OpenOption[] openOptions;
         private volatile AccessControlContext acc;
 
         public FileDownloadBodyHandler(Path directory, OpenOption... openOptions) {
@@ -137,25 +141,85 @@
             this.acc = acc;
         }
 
+        /** The "attachment" disposition-type and separator. */
+        static final String DISPOSITION_TYPE = "attachment;";
+
+        /** The "filename" parameter. */
+        static final Pattern FILENAME = Pattern.compile("filename\\s*=", CASE_INSENSITIVE);
+
+        static final List<String> PROHIBITED = List.of(".", "..", "", "~" , "|");
+
+        static final UncheckedIOException unchecked(int code,
+                                                    HttpHeaders headers,
+                                                    String msg) {
+            String s = String.format("%s in response [%d, %s]", msg, code, headers);
+            return new UncheckedIOException(new IOException(s));
+        }
+
         @Override
         public BodySubscriber<Path> apply(int statusCode, HttpHeaders headers) {
             String dispoHeader = headers.firstValue("Content-Disposition")
-                    .orElseThrow(() -> unchecked(new IOException("No Content-Disposition")));
-            if (!dispoHeader.startsWith("attachment;")) {
-                throw unchecked(new IOException("Unknown Content-Disposition type"));
+                    .orElseThrow(() -> unchecked(statusCode, headers,
+                            "No Content-Disposition header"));
+
+            if (!dispoHeader.regionMatches(true, // ignoreCase
+                                           0, DISPOSITION_TYPE,
+                                           0, DISPOSITION_TYPE.length())) {
+                throw unchecked(statusCode, headers, "Unknown Content-Disposition type");
+            }
+
+            Matcher matcher = FILENAME.matcher(dispoHeader);
+            if (!matcher.find()) {
+                throw unchecked(statusCode, headers,
+                          "Bad Content-Disposition filename parameter");
             }
-            int n = dispoHeader.indexOf("filename=");
-            if (n == -1) {
-                throw unchecked(new IOException("Bad Content-Disposition type"));
+            int n = matcher.end();
+
+            int semi = dispoHeader.substring(n).indexOf(";");
+            String filenameParam;
+            if (semi < 0) {
+                filenameParam = dispoHeader.substring(n);
+            } else {
+                filenameParam = dispoHeader.substring(n, n + semi);
+            }
+
+            // strip all but the last path segment
+            int x = filenameParam.lastIndexOf("/");
+            if (x != -1) {
+                filenameParam = filenameParam.substring(x+1);
             }
-            int lastsemi = dispoHeader.lastIndexOf(';');
-            String disposition;
-            if (lastsemi < n) {
-                disposition = dispoHeader.substring(n + 9);
-            } else {
-                disposition = dispoHeader.substring(n + 9, lastsemi);
+            x = filenameParam.lastIndexOf("\\");
+            if (x != -1) {
+                filenameParam = filenameParam.substring(x+1);
             }
-            Path file = Paths.get(directory.toString(), disposition);
+
+            filenameParam = filenameParam.trim();
+
+            if (filenameParam.startsWith("\"")) {  // quoted-string
+                if (!filenameParam.endsWith("\"") || filenameParam.length() == 1) {
+                    throw unchecked(statusCode, headers,
+                            "Badly quoted Content-Disposition filename parameter");
+                }
+                filenameParam = filenameParam.substring(1, filenameParam.length() -1 );
+            } else {  // token,
+                if (filenameParam.contains(" ")) {  // space disallowed
+                    throw unchecked(statusCode, headers,
+                            "unquoted space in Content-Disposition filename parameter");
+                }
+            }
+
+            if (PROHIBITED.contains(filenameParam)) {
+                throw unchecked(statusCode, headers,
+                        "Prohibited Content-Disposition filename parameter:"
+                                + filenameParam);
+            }
+
+            Path file = Paths.get(directory.toString(), filenameParam);
+
+            if (!file.startsWith(directory)) {
+                throw unchecked(statusCode, headers,
+                        "Resulting file, " + file.toString() + ", outside of given directory");
+            }
 
             PathSubscriber bs = (PathSubscriber)asFileImpl(file, openOptions);
             bs.setAccessControlContext(acc);