mirror of
https://github.com/meienberger/runtipi.git
synced 2024-10-26 20:19:56 +03:00
feat: sanitize paths when user input is used in path.join
This commit is contained in:
parent
d8934d7853
commit
edf27af127
4
packages/shared/src/helpers/sanitizers.ts
Normal file
4
packages/shared/src/helpers/sanitizers.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const sanitizePath = (inputPath: string) => {
|
||||
// Remove any sequence of "../" or "./" to prevent directory traversal
|
||||
return inputPath.replace(/(\.\.\/|\.\/)/g, '');
|
||||
};
|
@ -9,3 +9,4 @@ export { systemLoadSchema, type SystemLoad } from './schemas/system-schemas';
|
||||
// Helpers
|
||||
export { envMapToString, envStringToMap } from './helpers/env-helpers';
|
||||
export { cleanseErrorData } from './helpers/error-helpers';
|
||||
export { sanitizePath } from './helpers/sanitizers';
|
||||
|
@ -1,5 +1,6 @@
|
||||
import path from 'path';
|
||||
import { execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import { sanitizePath } from '@runtipi/shared';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
@ -22,13 +23,13 @@ const composeUp = async (args: string[]) => {
|
||||
*/
|
||||
export const compose = async (appId: string, command: string) => {
|
||||
const { arch, appsRepoId } = getEnv();
|
||||
const appDataDirPath = path.join(APP_DATA_DIR, appId);
|
||||
const appDirPath = path.join(DATA_DIR, 'apps', appId);
|
||||
const appDataDirPath = path.join(APP_DATA_DIR, sanitizePath(appId));
|
||||
const appDirPath = path.join(DATA_DIR, 'apps', sanitizePath(appId));
|
||||
|
||||
const args: string[] = [`--env-file ${path.join(appDataDirPath, 'app.env')}`];
|
||||
|
||||
// User custom env file
|
||||
const userEnvFile = path.join(DATA_DIR, 'user-config', appId, 'app.env');
|
||||
const userEnvFile = path.join(DATA_DIR, 'user-config', sanitizePath(appId), 'app.env');
|
||||
if (await pathExists(userEnvFile)) {
|
||||
args.push(`--env-file ${userEnvFile}`);
|
||||
}
|
||||
@ -45,7 +46,7 @@ export const compose = async (appId: string, command: string) => {
|
||||
args.push(`-f ${commonComposeFile}`);
|
||||
|
||||
// User defined overrides
|
||||
const userComposeFile = path.join(DATA_DIR, 'user-config', appId, 'docker-compose.yml');
|
||||
const userComposeFile = path.join(DATA_DIR, 'user-config', sanitizePath(appId), 'docker-compose.yml');
|
||||
if (await pathExists(userComposeFile)) {
|
||||
args.push(`--file ${userComposeFile}`);
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import { SocketEvent } from '@runtipi/shared';
|
||||
import { SocketEvent, sanitizePath } from '@runtipi/shared';
|
||||
import { copyDataDir, generateEnvFile } from './app.helpers';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { compose } from '@/lib/docker';
|
||||
@ -38,10 +38,10 @@ export class AppExecutors {
|
||||
private getAppPaths = (appId: string) => {
|
||||
const { appsRepoId } = getEnv();
|
||||
|
||||
const appDataDirPath = path.join(APP_DATA_DIR, appId);
|
||||
const appDirPath = path.join(DATA_DIR, 'apps', appId);
|
||||
const appDataDirPath = path.join(APP_DATA_DIR, sanitizePath(appId));
|
||||
const appDirPath = path.join(DATA_DIR, 'apps', sanitizePath(appId));
|
||||
const configJsonPath = path.join(appDirPath, 'config.json');
|
||||
const repoPath = path.join(DATA_DIR, 'repos', appsRepoId, 'apps', appId);
|
||||
const repoPath = path.join(DATA_DIR, 'repos', appsRepoId, 'apps', sanitizePath(appId));
|
||||
|
||||
return { appDataDirPath, appDirPath, configJsonPath, repoPath };
|
||||
};
|
||||
@ -53,7 +53,7 @@ export class AppExecutors {
|
||||
*/
|
||||
private ensureAppDir = async (appId: string) => {
|
||||
const { appDirPath, appDataDirPath, repoPath } = this.getAppPaths(appId);
|
||||
const dockerFilePath = path.join(DATA_DIR, 'apps', appId, 'docker-compose.yml');
|
||||
const dockerFilePath = path.join(DATA_DIR, 'apps', sanitizePath(appId), 'docker-compose.yml');
|
||||
|
||||
if (!(await pathExists(dockerFilePath))) {
|
||||
// delete eventual app folder if exists
|
||||
@ -104,7 +104,7 @@ export class AppExecutors {
|
||||
const { appDirPath, repoPath, appDataDirPath } = this.getAppPaths(appId);
|
||||
|
||||
// Check if app exists in repo
|
||||
const apps = await fs.promises.readdir(path.join(DATA_DIR, 'repos', appsRepoId, 'apps'));
|
||||
const apps = await fs.promises.readdir(path.join(DATA_DIR, 'repos', sanitizePath(appsRepoId), 'apps'));
|
||||
|
||||
if (!apps.includes(appId)) {
|
||||
this.logger.error(`App ${appId} not found in repo ${appsRepoId}`);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { appInfoSchema, envMapToString, envStringToMap } from '@runtipi/shared';
|
||||
import { appInfoSchema, envMapToString, envStringToMap, sanitizePath } from '@runtipi/shared';
|
||||
import { pathExists, execAsync } from '@runtipi/shared/node';
|
||||
import { generateVapidKeys, getAppEnvMap } from './env.helpers';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
@ -38,7 +38,7 @@ const getEntropy = async (name: string, length: number) => {
|
||||
export const generateEnvFile = async (appId: string, config: Record<string, unknown>) => {
|
||||
const { internalIp, storagePath, rootFolderHost } = getEnv();
|
||||
|
||||
const configFile = await fs.promises.readFile(path.join(DATA_DIR, 'apps', appId, 'config.json'));
|
||||
const configFile = await fs.promises.readFile(path.join(DATA_DIR, 'apps', sanitizePath(appId), 'config.json'));
|
||||
const parsedConfig = appInfoSchema.safeParse(JSON.parse(configFile.toString()));
|
||||
|
||||
if (!parsedConfig.success) {
|
||||
@ -52,7 +52,7 @@ export const generateEnvFile = async (appId: string, config: Record<string, unkn
|
||||
envMap.set('APP_PORT', String(parsedConfig.data.port));
|
||||
envMap.set('APP_ID', appId);
|
||||
envMap.set('ROOT_FOLDER_HOST', rootFolderHost);
|
||||
envMap.set('APP_DATA_DIR', path.join(storagePath, 'app-data', appId));
|
||||
envMap.set('APP_DATA_DIR', path.join(storagePath, 'app-data', sanitizePath(appId)));
|
||||
|
||||
const existingEnvMap = await getAppEnvMap(appId);
|
||||
|
||||
@ -101,12 +101,12 @@ export const generateEnvFile = async (appId: string, config: Record<string, unkn
|
||||
}
|
||||
|
||||
// Create app-data folder if it doesn't exist
|
||||
const appDataDirectoryExists = await fs.promises.stat(path.join(APP_DATA_DIR, appId)).catch(() => false);
|
||||
const appDataDirectoryExists = await fs.promises.stat(path.join(APP_DATA_DIR, sanitizePath(appId))).catch(() => false);
|
||||
if (!appDataDirectoryExists) {
|
||||
await fs.promises.mkdir(path.join(APP_DATA_DIR, appId), { recursive: true });
|
||||
await fs.promises.mkdir(path.join(APP_DATA_DIR, sanitizePath(appId)), { recursive: true });
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(path.join(APP_DATA_DIR, appId, 'app.env'), envMapToString(envMap));
|
||||
await fs.promises.writeFile(path.join(APP_DATA_DIR, sanitizePath(appId), 'app.env'), envMapToString(envMap));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -136,40 +136,41 @@ export const copyDataDir = async (id: string) => {
|
||||
const envMap = await getAppEnvMap(id);
|
||||
|
||||
// return if app does not have a data directory
|
||||
if (!(await pathExists(`${DATA_DIR}/apps/${id}/data`))) {
|
||||
if (!(await pathExists(path.join(DATA_DIR, 'apps', sanitizePath(id), 'data')))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create app-data folder if it doesn't exist
|
||||
if (!(await pathExists(`${APP_DATA_DIR}/${id}/data`))) {
|
||||
await fs.promises.mkdir(`${APP_DATA_DIR}/${id}/data`, { recursive: true });
|
||||
if (!(await pathExists(path.join(APP_DATA_DIR, sanitizePath(id), 'data')))) {
|
||||
await fs.promises.mkdir(path.join(APP_DATA_DIR, sanitizePath(id), 'data'), { recursive: true });
|
||||
}
|
||||
|
||||
const dataDir = await fs.promises.readdir(`${DATA_DIR}/apps/${id}/data`);
|
||||
const dataDir = await fs.promises.readdir(path.join(DATA_DIR, 'apps', sanitizePath(id), 'data'));
|
||||
|
||||
const processFile = async (file: string) => {
|
||||
if (file.endsWith('.template')) {
|
||||
const template = await fs.promises.readFile(`${DATA_DIR}/apps/${id}/data/${file}`, 'utf-8');
|
||||
const template = await fs.promises.readFile(path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', file), 'utf-8');
|
||||
const renderedTemplate = renderTemplate(template, envMap);
|
||||
|
||||
await fs.promises.writeFile(`${APP_DATA_DIR}/${id}/data/${file.replace('.template', '')}`, renderedTemplate);
|
||||
await fs.promises.writeFile(path.join(APP_DATA_DIR, sanitizePath(id), 'data', file.replace('.template', '')), renderedTemplate);
|
||||
} else {
|
||||
await fs.promises.copyFile(`${DATA_DIR}/apps/${id}/data/${file}`, `/app-data/${id}/data/${file}`);
|
||||
await fs.promises.copyFile(path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', file), path.join(APP_DATA_DIR, sanitizePath(id), 'data', file));
|
||||
}
|
||||
};
|
||||
|
||||
const processDir = async (p: string) => {
|
||||
await fs.promises.mkdir(`${APP_DATA_DIR}/${id}/data/${p}`, { recursive: true });
|
||||
const files = await fs.promises.readdir(`${DATA_DIR}/apps/${id}/data/${p}`);
|
||||
await fs.promises.mkdir(path.join(APP_DATA_DIR, sanitizePath(id), 'data', p), { recursive: true });
|
||||
|
||||
const files = await fs.promises.readdir(path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', p));
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const fullPath = `${DATA_DIR}/apps/${id}/data/${p}/${file}`;
|
||||
const fullPath = path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', p, file);
|
||||
|
||||
if ((await fs.promises.lstat(fullPath)).isDirectory()) {
|
||||
await processDir(`${p}/${file}`);
|
||||
await processDir(path.join(p, file));
|
||||
} else {
|
||||
await processFile(`${p}/${file}`);
|
||||
await processFile(path.join(p, file));
|
||||
}
|
||||
}),
|
||||
);
|
||||
@ -177,7 +178,7 @@ export const copyDataDir = async (id: string) => {
|
||||
|
||||
await Promise.all(
|
||||
dataDir.map(async (file) => {
|
||||
const fullPath = `${DATA_DIR}/apps/${id}/data/${file}`;
|
||||
const fullPath = path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', file);
|
||||
|
||||
if ((await fs.promises.lstat(fullPath)).isDirectory()) {
|
||||
await processDir(file);
|
||||
@ -188,7 +189,7 @@ export const copyDataDir = async (id: string) => {
|
||||
);
|
||||
|
||||
// Remove any .gitkeep files from the app-data folder at any level
|
||||
if (await pathExists(`${APP_DATA_DIR}/${id}/data`)) {
|
||||
await execAsync(`find ${APP_DATA_DIR}/${id}/data -name .gitkeep -delete`).catch(() => {});
|
||||
if (await pathExists(path.join(APP_DATA_DIR, sanitizePath(id), 'data'))) {
|
||||
await execAsync(`find ${APP_DATA_DIR}/${sanitizePath(id)}/data -name .gitkeep -delete`).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import webpush from 'web-push';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { sanitizePath } from '@runtipi/shared';
|
||||
import { APP_DATA_DIR } from '@/config/constants';
|
||||
|
||||
/**
|
||||
@ -11,7 +12,7 @@ import { APP_DATA_DIR } from '@/config/constants';
|
||||
*/
|
||||
export const getAppEnvMap = async (appId: string) => {
|
||||
try {
|
||||
const envFile = await fs.promises.readFile(path.join(APP_DATA_DIR, appId, 'app.env'));
|
||||
const envFile = await fs.promises.readFile(path.join(APP_DATA_DIR, sanitizePath(appId), 'app.env'));
|
||||
const envVars = envFile.toString().split('\n');
|
||||
const envVarsMap = new Map<string, string>();
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import path from 'path';
|
||||
import { execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { sanitizePath } from '@runtipi/shared';
|
||||
import { getRepoHash, getRepoBaseUrlAndBranch } from './repo.helpers';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { DATA_DIR } from '@/config/constants';
|
||||
@ -37,7 +38,7 @@ export class RepoExecutors {
|
||||
// We may have a potential branch computed in the hash (see getRepoBaseUrlAndBranch)
|
||||
// so we do it here before splitting the url into repoUrl and branch
|
||||
const repoHash = getRepoHash(url);
|
||||
const repoPath = path.join(DATA_DIR, 'repos', repoHash);
|
||||
const repoPath = path.join(DATA_DIR, 'repos', sanitizePath(repoHash));
|
||||
|
||||
if (await pathExists(repoPath)) {
|
||||
this.logger.info(`Repo ${url} already exists`);
|
||||
@ -76,7 +77,7 @@ export class RepoExecutors {
|
||||
public pullRepo = async (repoUrl: string) => {
|
||||
try {
|
||||
const repoHash = getRepoHash(repoUrl);
|
||||
const repoPath = path.join(DATA_DIR, 'repos', repoHash);
|
||||
const repoPath = path.join(DATA_DIR, 'repos', sanitizePath(repoHash));
|
||||
|
||||
if (!(await pathExists(repoPath))) {
|
||||
this.logger.info(`Repo ${repoUrl} does not exist`);
|
||||
|
@ -3,7 +3,8 @@ import { TipiConfig } from '@/server/core/TipiConfig/TipiConfig';
|
||||
import { pathExists } from '@runtipi/shared/node';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { APP_DIR, DATA_DIR } from 'src/config';
|
||||
import { sanitizePath } from '@runtipi/shared';
|
||||
import { APP_DIR, DATA_DIR } from '../../../config/constants';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
@ -14,8 +15,8 @@ export async function GET(request: Request) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
const defaultFilePath = path.join(DATA_DIR, 'apps', id, 'metadata', 'logo.jpg');
|
||||
const appRepoFilePath = path.join(DATA_DIR, 'repos', TipiConfig.getConfig().appsRepoId, 'apps', id, 'metadata', 'logo.jpg');
|
||||
const defaultFilePath = path.join(DATA_DIR, 'apps', sanitizePath(id), 'metadata', 'logo.jpg');
|
||||
const appRepoFilePath = path.join(DATA_DIR, 'repos', TipiConfig.getConfig().appsRepoId, 'apps', sanitizePath(id), 'metadata', 'logo.jpg');
|
||||
|
||||
let filePath = path.join(APP_DIR, 'public', 'app-not-found.jpg');
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user