Navigation Menu

Skip to content

Commit

Permalink
Add confine option for h.file() (#38)
Browse files Browse the repository at this point in the history
Defaults to the current working directory.
  • Loading branch information
sholladay committed Jun 27, 2020
1 parent 933c77b commit 2af2f39
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -5,4 +5,4 @@ install:
- export PATH="$HOME/.deno/bin:$PATH"

script:
- deno test --allow-read='.' test/*
- deno test --allow-read test/*
2 changes: 2 additions & 0 deletions dependencies.ts
Expand Up @@ -6,13 +6,15 @@ import * as cookie from 'https://deno.land/std@v0.56.0/http/cookie.ts';
import * as http from 'https://deno.land/std@v0.56.0/http/server.ts';
import { Status as status, STATUS_TEXT as statusText } from 'https://deno.land/std@v0.56.0/http/http_status.ts';
import * as mime from 'https://cdn.pika.dev/mime-types@2.1.27';
import * as path from 'https://deno.land/std@v0.56.0/path/mod.ts';

export {
React,
ReactDOMServer,
cookie,
http,
mime,
path,
status,
statusText
};
14 changes: 13 additions & 1 deletion lib/toolkit.ts
@@ -1,9 +1,21 @@
import Response from './response.ts';
import * as bang from './bang.ts';
import { ResponseBody } from './types.ts';
import isPathInside from './util/is-path-inside.ts';
import { mime } from '../dependencies.ts';

interface FileHandlerOptions {
confine: boolean | string
}

export default class Toolkit {
async file(path: string): Promise<Response> {
async file(path: string, options?: FileHandlerOptions): Promise<Response> {
if (options?.confine !== false) {
const confine = typeof options?.confine === 'string' ? options.confine : Deno.cwd();
if (!(await isPathInside.fs(path, confine))) {
throw bang.forbidden();
}
}
const file = await Deno.readFile(path);
const mediaType = mime.lookup(path);
const contentType = mime.contentType(mediaType || '');
Expand Down
33 changes: 33 additions & 0 deletions lib/util/is-path-inside.ts
@@ -0,0 +1,33 @@
import { path } from '../../dependencies.ts';

const realPathSilent = async (filePath: string): Promise<string> => {
try {
return await Deno.realPath(filePath);
}
catch (error) {
if (error instanceof Deno.errors.NotFound) {
return filePath;
}
throw error;
}
};

const isPathInside = (childPath: string, parentPath: string): boolean => {
const relation = path.relative(parentPath, childPath);
return Boolean(
relation &&
relation !== '..' &&
!relation.startsWith('..' + path.SEP) &&
relation !== path.resolve(childPath)
);
};

isPathInside.fs = async (childPath: string, parentPath: string): Promise<boolean> => {
const [realChildPath, realParentPath] = await Promise.all([
realPathSilent(childPath),
realPathSilent(parentPath)
]);
return isPathInside(realChildPath, realParentPath);
};

export default isPathInside;
22 changes: 22 additions & 0 deletions test/toolkit.js
Expand Up @@ -102,3 +102,25 @@ test('h.file()', async () => {
assertStrictEq(response.headers.get('content-type'), 'application/json; charset=utf-8');
assertEquals(response.body, new TextEncoder().encode('[\n "Alice",\n "Bob",\n "Cara"\n]\n'));
});

test('h.file() outside default confine', async () => {
const server = pogo.server();
server.route({
method : 'GET',
path : '/forbid',
handler(request, h) {
return h.file('/etc/hosts');
}
});
const response = await server.inject({
method : 'GET',
url : '/forbid'
});
assertStrictEq(response.status, 403);
assertStrictEq(response.headers.get('content-type'), 'application/json; charset=utf-8');
assertEquals(response.body, JSON.stringify({
error : 'Forbidden',
message : 'Forbidden',
status : 403
}));
});

0 comments on commit 2af2f39

Please sign in to comment.