assert normalized

This commit is contained in:
ProgrammerIn-wonderland
2026-05-15 16:24:36 -04:00
parent 54b61f2e24
commit fdf8a2e3a3
8 changed files with 28 additions and 12 deletions
+3 -1
View File
@@ -20,6 +20,7 @@
import Busboy from 'busboy';
import type { Request, Response } from 'express';
import { posix as pathPosix } from 'node:path';
import { assertNormalized } from '../../services/fs/resolveNode.js';
import { pipeline } from 'node:stream/promises';
import type { Actor } from '../../core/actor.js';
import { Context } from '../../core/context.js';
@@ -1787,7 +1788,8 @@ export class FSController extends PuterController {
pathToNormalize = `/${username}${pathToNormalize.slice(1)}`;
}
let normalizedPath = pathPosix.normalize(pathToNormalize);
assertNormalized(pathToNormalize);
let normalizedPath = pathToNormalize;
if (!normalizedPath.startsWith('/')) {
normalizedPath = `/${normalizedPath}`;
}
@@ -21,6 +21,7 @@ import type { RequestHandler } from 'express';
import { stat } from 'node:fs/promises';
import path from 'node:path';
import type { IConfig } from '../../../types';
import { assertNormalized } from '../../../services/fs/resolveNode.js';
/** Native-app subdomains served via `nativeAppStatic`. */
const NATIVE_APP_SUBDOMAINS = [
@@ -130,10 +131,8 @@ export const createNativeAppStatic = (config: IConfig): RequestHandler => {
? path.join(root, active, 'dist')
: path.join(root, active);
// req.path is already url-decoded by express; normalize strips any
// `..` segments before sendFile's `root` option enforces its own
// traversal guard.
const requested = path.normalize(req.path);
const requested = req.path;
assertNormalized(requested);
const absolute = path.join(appRoot, requested);
try {
@@ -19,6 +19,7 @@
import crypto from 'node:crypto';
import { posix as pathPosix } from 'node:path';
import { assertNormalized } from '../../services/fs/resolveNode.js';
import { Readable } from 'node:stream';
import { Context } from '../../core/context.js';
import { HttpError } from '../../core/http/HttpError.js';
@@ -419,7 +420,7 @@ export class ImageGenerationDriver extends PuterDriver {
if (resolved === '~' || resolved.startsWith('~/')) {
resolved = `/${username}${resolved.slice(1)}`;
}
resolved = pathPosix.normalize(resolved);
assertNormalized(resolved);
if (!resolved.startsWith('/')) {
resolved = `/${resolved}`;
}
@@ -18,6 +18,7 @@
*/
import { posix as pathPosix } from 'node:path';
import { assertNormalized } from '../../services/fs/resolveNode.js';
import { Readable } from 'node:stream';
import { Context } from '../../core/context.js';
import { HttpError } from '../../core/http/HttpError.js';
@@ -458,7 +459,7 @@ export class VideoGenerationDriver extends PuterDriver {
if (resolved === '~' || resolved.startsWith('~/')) {
resolved = `/${username}${resolved.slice(1)}`;
}
resolved = pathPosix.normalize(resolved);
assertNormalized(resolved);
if (!resolved.startsWith('/')) {
resolved = `/${resolved}`;
}
+1 -2
View File
@@ -112,6 +112,7 @@ export class ACLService extends PuterService {
if (resource.path === '/') {
return (PUBLIC_READ_MODES as AclMode[]).includes(mode);
}
const ancestors = await resource.resolveAncestors();
const components = resource.path.slice(1).split('/');
@@ -171,7 +172,6 @@ export class ACLService extends PuterService {
const authorizer = actor.accessToken.issuer;
if (!(await this.check(authorizer, resource, mode))) return false;
const ancestors = await resource.resolveAncestors();
for (const ancestor of ancestors) {
const permissions =
mode === MANAGE_PERM_PREFIX
@@ -219,7 +219,6 @@ export class ACLService extends PuterService {
// Fall back to the permission scan: walk ancestors, any hit wins.
// Widen the scan to all "higher" modes (`write` covers `read`/`list`/
// `see`, etc.) so granting a stronger mode implies the weaker ones.
const ancestors = await resource.resolveAncestors();
for (const ancestor of ancestors) {
const permissions =
mode === MANAGE_PERM_PREFIX
+3 -1
View File
@@ -18,6 +18,7 @@
*/
import { posix as pathPosix } from 'node:path';
import { assertNormalized } from './resolveNode.js';
import { createHash } from 'node:crypto';
import { Readable, Transform } from 'node:stream';
import type { TransformCallback } from 'node:stream';
@@ -268,7 +269,8 @@ export class FSService extends PuterService {
);
}
let normalizedPath = pathPosix.normalize(trimmedPath);
assertNormalized(trimmedPath);
let normalizedPath = trimmedPath;
if (!normalizedPath.startsWith('/')) {
normalizedPath = `/${normalizedPath}`;
}
+11 -1
View File
@@ -130,6 +130,15 @@ export function splitParentAndName(absolutePath: string): {
return { parentPath: parentPath === '.' ? '/' : parentPath, name };
}
export function assertNormalized(input: string): string {
if (pathPosix.normalize(input) !== input) {
throw new HttpError(400, 'Invalid path', {
legacyCode: 'bad_request',
});
}
return input;
}
export function normalizeAbsolutePath(path: string): string {
const trimmed = typeof path === 'string' ? path.trim() : '';
if (trimmed.length === 0) {
@@ -137,7 +146,8 @@ export function normalizeAbsolutePath(path: string): string {
legacyCode: 'bad_request',
});
}
let normalized = pathPosix.normalize(trimmed);
assertNormalized(trimmed);
let normalized = trimmed;
if (!normalized.startsWith('/')) {
normalized = `/${normalized}`;
}
+3 -1
View File
@@ -19,6 +19,7 @@
import { statfs } from 'node:fs/promises';
import { posix as pathPosix } from 'node:path';
import { assertNormalized } from '../../services/fs/resolveNode.js';
import { v4 as uuidv4 } from 'uuid';
import { HttpError } from '../../core/http/HttpError.js';
import type { LayerInstances } from '../../types.js';
@@ -175,7 +176,8 @@ export class FSEntryStore extends PuterStore {
});
}
let normalized = pathPosix.normalize(trimmed);
assertNormalized(trimmed);
let normalized = trimmed;
if (!normalized.startsWith('/')) {
normalized = `/${normalized}`;
}