mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 09:31:22 -05:00
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).
This commit is contained in:
parent
4f46dc999b
commit
28869a632d
2 changed files with 127 additions and 6 deletions
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue