mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-12 18:39:29 +08:00
* fix(security): restrict task environment controls and app scopes * fix(security): preserve legacy scopes and add read-only access * feat(security): add read-only scopes for remaining resources * fix(security): reject apps scope grants in app API * fix(security): protect system app and task file boundaries * fix: block case-variant secret paths and task runner env overrides
61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
function isWithin(root: string, target: string): boolean {
|
|
const relative = path.relative(root, target);
|
|
return (
|
|
relative === '' ||
|
|
(!relative.startsWith(`..${path.sep}`) &&
|
|
relative !== '..' &&
|
|
!path.isAbsolute(relative))
|
|
);
|
|
}
|
|
|
|
/** Resolve existing files and not-yet-created children without following an
|
|
* existing symlink outside the root. Blacklisted directories cover descendants. */
|
|
export function resolveFileAccess(
|
|
root: string,
|
|
parts: string[],
|
|
blacklist: string[] = [],
|
|
): string {
|
|
if (parts.some((part) => typeof part !== 'string' || part.includes('\0'))) {
|
|
return '';
|
|
}
|
|
const resolvedRoot = path.resolve(root);
|
|
const target = path.resolve(resolvedRoot, ...parts);
|
|
if (target === resolvedRoot || !isWithin(resolvedRoot, target)) return '';
|
|
// File systems such as default APFS can resolve a differently cased spelling
|
|
// of the same entry. Check every protected name without case sensitivity.
|
|
const blockedNames = new Set(blacklist.map((part) => part.toLowerCase()));
|
|
const isBlocked = (relative: string) =>
|
|
relative.split(path.sep).some((part) => blockedNames.has(part.toLowerCase()));
|
|
if (isBlocked(path.relative(resolvedRoot, target))) return '';
|
|
try {
|
|
const realRoot = fs.realpathSync(resolvedRoot);
|
|
let existing = target;
|
|
const missing: string[] = [];
|
|
while (!fs.existsSync(existing)) {
|
|
// existsSync is false for a dangling symlink; never treat one as absent.
|
|
try {
|
|
fs.lstatSync(existing);
|
|
return '';
|
|
} catch (error: any) {
|
|
if (error.code !== 'ENOENT') return '';
|
|
}
|
|
if (existing === resolvedRoot) return '';
|
|
missing.unshift(path.basename(existing));
|
|
existing = path.dirname(existing);
|
|
}
|
|
const realTarget = path.resolve(fs.realpathSync(existing), ...missing);
|
|
if (
|
|
!isWithin(realRoot, realTarget) ||
|
|
isBlocked(path.relative(realRoot, realTarget))
|
|
) {
|
|
return '';
|
|
}
|
|
return target;
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|