mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-04 19:08:40 +02:00
fix(security): harden YAML parsing, path traversal, and markdown links
- workflowSerialization: restrict YAML parser to core schema with no custom tags - gitService: validate file paths stay within project directory before I/O - chatMarkdown: allowlist URL protocols (https, http, mailto, anchors) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { isUtf8 } from 'node:buffer';
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { createRequire } from 'node:module';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import type {
|
||||
@@ -32,6 +32,16 @@ const { execFile } = require('node:child_process') as typeof import('node:child_
|
||||
const execFileAsync = promisify(execFile);
|
||||
const GIT_TIMEOUT_MS = 5_000;
|
||||
|
||||
/** Ensure `filePath` resolves inside `basePath`; throws on traversal. */
|
||||
function assertPathInsideBase(basePath: string, filePath: string): string {
|
||||
const resolved = resolve(basePath, filePath);
|
||||
const rel = relative(resolve(basePath), resolved);
|
||||
if (rel.startsWith('..') || isAbsolute(rel)) {
|
||||
throw new Error(`Path traversal detected: "${filePath}" escapes base directory.`);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
type GitCommandRunner = (projectPath: string, args: string[]) => Promise<string>;
|
||||
|
||||
type GitCommandResult =
|
||||
@@ -715,7 +725,7 @@ export class GitService {
|
||||
|
||||
if (isPureUntrackedFile(file)) {
|
||||
try {
|
||||
const contents = await readFile(join(projectPath, file.path));
|
||||
const contents = await readFile(assertPathInsideBase(projectPath, file.path));
|
||||
return {
|
||||
path: file.path,
|
||||
previousPath: file.previousPath,
|
||||
@@ -813,7 +823,7 @@ export class GitService {
|
||||
throw unstageResult.error;
|
||||
}
|
||||
|
||||
await rm(join(projectPath, path), { force: true });
|
||||
await rm(assertPathInsideBase(projectPath, path), { force: true });
|
||||
}
|
||||
|
||||
private async applyPatch(projectPath: string, diff: string): Promise<void> {
|
||||
|
||||
@@ -100,10 +100,15 @@ export const chatMarkdownComponents: Components = {
|
||||
},
|
||||
|
||||
a({ href, children }) {
|
||||
const url = String(href ?? '');
|
||||
const isSafe = /^https?:|^mailto:|^#/i.test(url);
|
||||
if (!isSafe) {
|
||||
return <span className="text-indigo-400">{children as ReactNode}</span>;
|
||||
}
|
||||
return (
|
||||
<a
|
||||
className="text-indigo-400 underline underline-offset-2 transition hover:text-indigo-300"
|
||||
href={String(href)}
|
||||
href={url}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -177,9 +177,15 @@ export function exportWorkflowDefinition(
|
||||
}
|
||||
}
|
||||
|
||||
const safeYamlOptions = {
|
||||
schema: 'core' as const,
|
||||
customTags: [] as [],
|
||||
merge: false as const,
|
||||
};
|
||||
|
||||
export function importWorkflowDefinition(content: string, format: 'yaml' | 'json'): WorkflowDefinition {
|
||||
const parsed = format === 'yaml'
|
||||
? parseYaml(content)
|
||||
? parseYaml(content, safeYamlOptions)
|
||||
: JSON.parse(content) as unknown;
|
||||
|
||||
return ensureValidWorkflow(coerceWorkflowDefinition(parsed));
|
||||
|
||||
Reference in New Issue
Block a user