mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-02 19:01:55 +00:00
TASK-3909cbf6-10c6-44ce-b82e-5bfb62cf34c8 harden artifact target guard
This commit is contained in:
1 parent
014bdb4f97
commit
0fdfcb3b68
4 files changed
+92
-24
No files matched your search
@@ -1,12 +1,12 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
||||
import { existsSync, lstatSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, relative, resolve } from 'node:path';
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
|
||||
function isWithin(path, root) {
|
||||
const difference = relative(root, path);
|
||||
return difference === '' || (!difference.startsWith('..') && !difference.includes('/../'));
|
||||
return difference === '' || (!isAbsolute(difference) && difference !== '..' && !difference.startsWith(`..${sep}`));
|
||||
}
|
||||
|
||||
function existingAncestor(path) {
|
||||
@@ -36,6 +36,28 @@ function refusal(target, protectedRoot) {
|
||||
return error;
|
||||
}
|
||||
|
||||
function targetExists(target) {
|
||||
try {
|
||||
lstatSync(target);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePhysicalDirectory(directory) {
|
||||
const requested = resolve(directory);
|
||||
const ancestor = existingAncestor(requested);
|
||||
return { requested, resolved: resolve(realpathSync(ancestor), relative(ancestor, requested)) };
|
||||
}
|
||||
|
||||
function assertExternalDirectory({ requested, resolved }, repositoryRoot) {
|
||||
for (const protectedRoot of trackedWorktreeRoots(repositoryRoot)) {
|
||||
if (isWithin(resolved, protectedRoot)) throw refusal(requested, protectedRoot);
|
||||
}
|
||||
}
|
||||
|
||||
export function createDashboardArtifactDirectory({
|
||||
environment = process.env,
|
||||
repositoryRoot,
|
||||
@@ -44,18 +66,33 @@ export function createDashboardArtifactDirectory({
|
||||
if (!repositoryRoot) throw new Error('repositoryRoot is required for dashboard artifact isolation');
|
||||
const requestedDirectory = environment.TORCH_CHECK_ARTIFACT_DIR;
|
||||
if (!requestedDirectory) {
|
||||
const directory = mkdtempSync(join(temporaryRoot, 'torch-dashboard-artifacts-'));
|
||||
const temporary = resolvePhysicalDirectory(temporaryRoot);
|
||||
assertExternalDirectory(temporary, repositoryRoot);
|
||||
const directory = mkdtempSync(join(temporary.resolved, 'torch-dashboard-artifacts-'));
|
||||
return { directory, provenance: 'standalone-temporary', requestedDirectory: null };
|
||||
}
|
||||
|
||||
const requested = resolve(requestedDirectory);
|
||||
const ancestor = existingAncestor(requested);
|
||||
const resolved = resolve(realpathSync(ancestor), relative(ancestor, requested));
|
||||
for (const protectedRoot of trackedWorktreeRoots(repositoryRoot)) {
|
||||
if (isWithin(resolved, protectedRoot)) throw refusal(requested, protectedRoot);
|
||||
const output = resolvePhysicalDirectory(requestedDirectory);
|
||||
assertExternalDirectory(output, repositoryRoot);
|
||||
mkdirSync(output.resolved, { recursive: true });
|
||||
return { directory: output.resolved, provenance: 'TORCH_CHECK_ARTIFACT_DIR', requestedDirectory: output.requested };
|
||||
}
|
||||
|
||||
export function prepareDashboardArtifactTargets(artifacts, names) {
|
||||
if (new Set(names).size !== names.length || names.some((name) => name !== basename(name))) {
|
||||
throw new Error('Dashboard screenshot names must be unique plain filenames');
|
||||
}
|
||||
mkdirSync(resolved, { recursive: true });
|
||||
return { directory: resolved, provenance: 'TORCH_CHECK_ARTIFACT_DIR', requestedDirectory: requested };
|
||||
const paths = {};
|
||||
for (const name of names) {
|
||||
const target = join(artifacts.directory, name);
|
||||
if (targetExists(target)) {
|
||||
const error = new Error(`DASHBOARD_ARTIFACT_TARGET_EXISTS: refusing to replace ${target}`);
|
||||
error.code = 'DASHBOARD_ARTIFACT_TARGET_EXISTS';
|
||||
throw error;
|
||||
}
|
||||
paths[name] = target;
|
||||
}
|
||||
return Object.freeze(paths);
|
||||
}
|
||||
|
||||
function artifactFiles(directory, prefix = '') {
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { once } from 'node:events';
|
||||
import { chromium } from '@playwright/test';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createConsoleServer } from '../src/console/server.mjs';
|
||||
import { createDashboardArtifactDirectory, describeDashboardArtifacts } from './dashboard-artifacts.mjs';
|
||||
import { createDashboardArtifactDirectory, describeDashboardArtifacts, prepareDashboardArtifactTargets } from './dashboard-artifacts.mjs';
|
||||
|
||||
const screenshotNames = [
|
||||
'torch-dashboard-operation-outcomes.png', 'torch-dashboard-activity-review.png',
|
||||
'torch-dashboard-check-evidence.png', 'torch-dashboard-owner-briefing-desktop.png',
|
||||
'torch-dashboard-pilot-conclusion.png', 'torch-dashboard-pilot-review.png',
|
||||
'torch-dashboard-manager-wakes.png', 'torch-dashboard-demo-desktop.png',
|
||||
'torch-dashboard-demo-mobile.png', 'torch-dashboard-owner-briefing-mobile.png',
|
||||
'torch-dashboard-live-refresh.png',
|
||||
];
|
||||
const artifacts = createDashboardArtifactDirectory({
|
||||
repositoryRoot: fileURLToPath(new URL('..', import.meta.url)),
|
||||
});
|
||||
const screenshotDirectory = artifacts.directory;
|
||||
const screenshotPaths = prepareDashboardArtifactTargets(artifacts, screenshotNames);
|
||||
function screenshotPath(name) {
|
||||
return join(screenshotDirectory, name);
|
||||
return screenshotPaths[name];
|
||||
}
|
||||
|
||||
const server = createConsoleServer();
|
||||
|
||||
Reference in new issue
Block a user