summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsarahdenofiletrav <74922000+sarahdenofiletrav@users.noreply.github.com>2020-11-26 21:31:19 +0000
committerGitHub <noreply@github.com>2020-11-26 22:31:19 +0100
commit28869a632d190dc29d78738bc5e90eadf99bc824 (patch)
tree85b749f6ffbaeada706a6b6d15b0f4ce2399d454
parent4f46dc999b9fd3f26b6586d06d099d7039ca35c8 (diff)
fix(std/http): prevent path traversal (#8474)
Fix path traversal problem when the request URI does not have a leading slash. The file server now returns HTTP 400 when requests lack the leading slash, and are not absolute URIs. (https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html).
-rw-r--r--std/http/file_server.ts32
-rw-r--r--std/http/file_server_test.ts101
2 files changed, 127 insertions, 6 deletions
diff --git a/std/http/file_server.ts b/std/http/file_server.ts
index 331dbe5c5..c0d58351b 100644
--- a/std/http/file_server.ts
+++ b/std/http/file_server.ts
@@ -187,10 +187,15 @@ async function serveDir(
}
function serveFallback(req: ServerRequest, e: Error): Promise<Response> {
- if (e instanceof Deno.errors.NotFound) {
+ if (e instanceof URIError) {
+ return Promise.resolve({
+ status: 400,
+ body: encoder.encode("Bad Request"),
+ });
+ } else if (e instanceof Deno.errors.NotFound) {
return Promise.resolve({
status: 404,
- body: encoder.encode("Not found"),
+ body: encoder.encode("Not Found"),
});
} else {
return Promise.resolve({
@@ -335,6 +340,21 @@ function normalizeURL(url: string): string {
throw e;
}
}
+
+ try {
+ //allowed per https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
+ const absoluteURI = new URL(normalizedUrl);
+ normalizedUrl = absoluteURI.pathname;
+ } catch (e) { //wasn't an absoluteURI
+ if (!(e instanceof TypeError)) {
+ throw e;
+ }
+ }
+
+ if (normalizedUrl[0] !== "/") {
+ throw new URIError("The request URI is malformed.");
+ }
+
normalizedUrl = posix.normalize(normalizedUrl);
const startOfParams = normalizedUrl.indexOf("?");
return startOfParams > -1
@@ -383,11 +403,13 @@ function main(): void {
}
const handler = async (req: ServerRequest): Promise<void> => {
- const normalizedUrl = normalizeURL(req.url);
- const fsPath = posix.join(target, normalizedUrl);
-
let response: Response | undefined;
try {
+ const normalizedUrl = normalizeURL(req.url);
+ let fsPath = posix.join(target, normalizedUrl);
+ if (fsPath.indexOf(target) !== 0) {
+ fsPath = target;
+ }
const fileInfo = await Deno.stat(fsPath);
if (fileInfo.isDirectory) {
if (dirListingEnabled) {
diff --git a/std/http/file_server_test.ts b/std/http/file_server_test.ts
index 5f7137998..050109fe0 100644
--- a/std/http/file_server_test.ts
+++ b/std/http/file_server_test.ts
@@ -2,11 +2,12 @@
import {
assert,
assertEquals,
+ assertNotEquals,
assertStringIncludes,
} from "../testing/asserts.ts";
import { BufReader } from "../io/bufio.ts";
import { TextProtoReader } from "../textproto/mod.ts";
-import { ServerRequest } from "./server.ts";
+import { Response, ServerRequest } from "./server.ts";
import { FileServerArgs, serveFile } from "./file_server.ts";
import { dirname, fromFileUrl, join, resolve } from "../path/mod.ts";
let fileServer: Deno.Process<Deno.RunOptions & { stdout: "piped" }>;
@@ -78,6 +79,78 @@ async function killFileServer(): Promise<void> {
fileServer.stdout!.close();
}
+interface StringResponse extends Response {
+ body: string;
+}
+
+/* HTTP GET request allowing arbitrary paths */
+async function fetchExactPath(
+ hostname: string,
+ port: number,
+ path: string,
+): Promise<StringResponse> {
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+ const request = encoder.encode("GET " + path + " HTTP/1.1\r\n\r\n");
+ let conn: void | Deno.Conn;
+ try {
+ conn = await Deno.connect(
+ { hostname: hostname, port: port, transport: "tcp" },
+ );
+ await Deno.writeAll(conn, request);
+ let currentResult = "";
+ let contentLength = -1;
+ let startOfBody = -1;
+ for await (const chunk of Deno.iter(conn)) {
+ currentResult += decoder.decode(chunk);
+ if (contentLength === -1) {
+ const match = /^content-length: (.*)$/m.exec(currentResult);
+ if (match && match[1]) {
+ contentLength = Number(match[1]);
+ }
+ }
+ if (startOfBody === -1) {
+ const ind = currentResult.indexOf("\r\n\r\n");
+ if (ind !== -1) {
+ startOfBody = ind + 4;
+ }
+ }
+ if (startOfBody !== -1 && contentLength !== -1) {
+ const byteLen = encoder.encode(currentResult).length;
+ if (byteLen >= contentLength + startOfBody) {
+ break;
+ }
+ }
+ }
+ const status = /^HTTP\/1.1 (...)/.exec(currentResult);
+ let statusCode = 0;
+ if (status && status[1]) {
+ statusCode = Number(status[1]);
+ }
+
+ const body = currentResult.slice(startOfBody);
+ const headersStr = currentResult.slice(0, startOfBody);
+ const headersReg = /^(.*): (.*)$/mg;
+ const headersObj: { [i: string]: string } = {};
+ let match = headersReg.exec(headersStr);
+ while (match !== null) {
+ if (match[1] && match[2]) {
+ headersObj[match[1]] = match[2];
+ }
+ match = headersReg.exec(headersStr);
+ }
+ return {
+ status: statusCode,
+ headers: new Headers(headersObj),
+ body: body,
+ };
+ } finally {
+ if (conn) {
+ Deno.close(conn.rid);
+ }
+ }
+}
+
Deno.test(
"file_server serveFile",
async (): Promise<void> => {
@@ -169,6 +242,32 @@ Deno.test("checkPathTraversal", async function (): Promise<void> {
}
});
+Deno.test("checkPathTraversalNoLeadingSlash", async function (): Promise<void> {
+ await startFileServer();
+ try {
+ const res = await fetchExactPath("127.0.0.1", 4507, "../../../..");
+ assertEquals(res.status, 400);
+ } finally {
+ await killFileServer();
+ }
+});
+
+Deno.test("checkPathTraversalAbsoluteURI", async function (): Promise<void> {
+ await startFileServer();
+ try {
+ //allowed per https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
+ const res = await fetchExactPath(
+ "127.0.0.1",
+ 4507,
+ "http://localhost/../../../..",
+ );
+ assertEquals(res.status, 200);
+ assertStringIncludes(res.body, "README.md");
+ } finally {
+ await killFileServer();
+ }
+});
+
Deno.test("checkURIEncodedPathTraversal", async function (): Promise<void> {
await startFileServer();
try {