mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-08 20:58:45 +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 { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
import { tmpdir } from 'node:os';
|
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 { promisify } from 'node:util';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -32,6 +32,16 @@ const { execFile } = require('node:child_process') as typeof import('node:child_
|
|||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
const GIT_TIMEOUT_MS = 5_000;
|
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 GitCommandRunner = (projectPath: string, args: string[]) => Promise<string>;
|
||||||
|
|
||||||
type GitCommandResult =
|
type GitCommandResult =
|
||||||
@@ -715,7 +725,7 @@ export class GitService {
|
|||||||
|
|
||||||
if (isPureUntrackedFile(file)) {
|
if (isPureUntrackedFile(file)) {
|
||||||
try {
|
try {
|
||||||
const contents = await readFile(join(projectPath, file.path));
|
const contents = await readFile(assertPathInsideBase(projectPath, file.path));
|
||||||
return {
|
return {
|
||||||
path: file.path,
|
path: file.path,
|
||||||
previousPath: file.previousPath,
|
previousPath: file.previousPath,
|
||||||
@@ -813,7 +823,7 @@ export class GitService {
|
|||||||
throw unstageResult.error;
|
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> {
|
private async applyPatch(projectPath: string, diff: string): Promise<void> {
|
||||||
|
|||||||
@@ -100,10 +100,15 @@ export const chatMarkdownComponents: Components = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
a({ href, children }) {
|
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 (
|
return (
|
||||||
<a
|
<a
|
||||||
className="text-indigo-400 underline underline-offset-2 transition hover:text-indigo-300"
|
className="text-indigo-400 underline underline-offset-2 transition hover:text-indigo-300"
|
||||||
href={String(href)}
|
href={url}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
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 {
|
export function importWorkflowDefinition(content: string, format: 'yaml' | 'json'): WorkflowDefinition {
|
||||||
const parsed = format === 'yaml'
|
const parsed = format === 'yaml'
|
||||||
? parseYaml(content)
|
? parseYaml(content, safeYamlOptions)
|
||||||
: JSON.parse(content) as unknown;
|
: JSON.parse(content) as unknown;
|
||||||
|
|
||||||
return ensureValidWorkflow(coerceWorkflowDefinition(parsed));
|
return ensureValidWorkflow(coerceWorkflowDefinition(parsed));
|
||||||
|
|||||||
Reference in New Issue
Block a user