diff --git a/server/routes/api/torrents.ts b/server/routes/api/torrents.ts index 3a96f3d63..84cbcc537 100644 --- a/server/routes/api/torrents.ts +++ b/server/routes/api/torrents.ts @@ -35,9 +35,17 @@ import { reannounceTorrentsSchema, setTorrentsTagsSchema, } from '../../../shared/schema/api/torrents'; -import {accessDeniedError, fileNotFoundError, isAllowedPath, sanitizePath} from '../../util/fileUtil'; +import { + accessDeniedError, + existAsync, + fileNotFoundError, + isAllowedPath, + isAllowedPathAsync, + sanitizePath, +} from '../../util/fileUtil'; import {getTempPath} from '../../models/TemporaryStorage'; import {getToken} from '../../util/authUtil'; +import {asyncFilter} from '../../util/async'; const getDestination = async ( services: Express.Request['services'], @@ -891,13 +899,13 @@ router.get<{hash: string}>( sanitizePath(path.join(torrentDirectory, content.path)), ); - torrentContentPaths = torrentContentPaths.filter((contentPath) => isAllowedPath(contentPath)); + torrentContentPaths = await asyncFilter(torrentContentPaths, (contentPath) => isAllowedPathAsync(contentPath)); if (torrentContentPaths.length < 1) { const {code, message} = accessDeniedError(); return res.status(403).json({code, message}); } - torrentContentPaths = torrentContentPaths.filter((contentPath) => fs.existsSync(contentPath)); + torrentContentPaths = await asyncFilter(torrentContentPaths, (contentPath) => existAsync(contentPath)); if (torrentContentPaths.length < 1) { const {code, message} = fileNotFoundError(); return res.status(404).json({code, message}); diff --git a/server/util/async.ts b/server/util/async.ts new file mode 100644 index 000000000..b104bf99d --- /dev/null +++ b/server/util/async.ts @@ -0,0 +1,14 @@ +export async function asyncFilter( + array: Array, + predicate: (item: T, index: number) => Promise, +): Promise> { + const results: T[] = []; + + for (const [index, item] of array.entries()) { + if (await predicate(item, index)) { + results.push(item); + } + } + + return results; +} diff --git a/server/util/fileUtil.ts b/server/util/fileUtil.ts index c2b6a58c9..0e7617e15 100644 --- a/server/util/fileUtil.ts +++ b/server/util/fileUtil.ts @@ -1,4 +1,5 @@ import fs from 'fs'; +import {promises as fsp} from 'fs'; import {homedir} from 'os'; import path from 'path'; @@ -43,6 +44,46 @@ export const isAllowedPath = (resolvedPath: string) => { }); }; +export async function isAllowedPathAsync(resolvedPath: string) { + if (config.allowedPaths == null) { + return true; + } + + let realPath: string | null = null; + let parentPath: string = resolvedPath; + while (realPath == null) { + try { + realPath = await fsp.realpath(parentPath); + } catch (e) { + if ((e as NodeJS.ErrnoException).code === 'ENOENT') { + parentPath = path.resolve(parentPath, '..'); + } else { + return false; + } + } + } + + return config.allowedPaths.some((allowedPath) => { + if (realPath?.startsWith(allowedPath)) { + return true; + } + return false; + }); +} + +export async function existAsync(path: string): Promise { + try { + await fsp.stat(path); + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + return false; + } + throw err; + } + + return true; +} + export const sanitizePath = (input?: string): string => { if (typeof input !== 'string') { throw accessDeniedError();