Compare commits

...

3 commits

Author SHA1 Message Date
dev
0c512a0cbc Merge pull request 'DIS-005: Path-Traversal-Check in /new-agent' (#25) from phase-0/path-traversal-check into main
Reviewed-on: #25
2026-04-09 09:44:54 +00:00
dev
613758e912 Merge branch 'main' into phase-0/path-traversal-check 2026-04-09 09:43:41 +00:00
Nick Tabeling
b4cc00e93c fix(new-agent): add path traversal containment check
Extracts validateAgentName() with path.resolve containment guard after
regex validation; returns clean ephemeral Discord reply on violation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 11:19:28 +02:00
2 changed files with 55 additions and 0 deletions

View file

@ -12,6 +12,21 @@ import { setupAgentWorkspace } from "../agent/identity";
// Allowed characters for agent names: lowercase letters, numbers, hyphens // Allowed characters for agent names: lowercase letters, numbers, hyphens
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$/; const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$/;
/**
* Validates that an agent name is safe and does not escape the workspaces root
* via path traversal. Throws an Error if the resolved path would lie outside root.
*/
export function validateAgentName(name: string, workspacesRoot: string): void {
if (!name) {
throw new Error(`Ungültiger Agent-Name: "${name}"`);
}
const root = path.resolve(workspacesRoot);
const wsPath = path.resolve(root, name);
if (!wsPath.startsWith(root + path.sep)) {
throw new Error(`Ungültiger Agent-Name: "${name}"`);
}
}
export const newAgentCommand = new SlashCommandBuilder() export const newAgentCommand = new SlashCommandBuilder()
.setName("new-agent") .setName("new-agent")
.setDescription("Erstellt einen neuen Agenten mit eigenem Kanal und Workspace") .setDescription("Erstellt einen neuen Agenten mit eigenem Kanal und Workspace")
@ -48,6 +63,15 @@ export async function handleNewAgent(
return; return;
} }
// Path-traversal containment check
try {
validateAgentName(name, config.workspaces_root);
} catch (error) {
const msg = error instanceof Error ? error.message : "Ungültiger Agent-Name";
await interaction.reply({ content: msg, ephemeral: true });
return;
}
// Check uniqueness // Check uniqueness
if (db.getWorkspaceByName(name)) { if (db.getWorkspaceByName(name)) {
await interaction.reply({ await interaction.reply({

View file

@ -0,0 +1,31 @@
import { describe, it, expect } from "vitest";
import { validateAgentName } from "../../src/commands/new-agent";
const ROOT = "/tmp/ws";
describe("validateAgentName path traversal containment", () => {
it('throws for "../etc"', () => {
expect(() => validateAgentName("../etc", ROOT)).toThrow();
});
it('throws for ".."', () => {
expect(() => validateAgentName("..", ROOT)).toThrow();
});
it('does not throw for "a/../b" (resolves to /tmp/ws/b, stays inside root)', () => {
// path.resolve("/tmp/ws", "a/../b") => "/tmp/ws/b" -- still inside root
expect(() => validateAgentName("a/../b", ROOT)).not.toThrow();
});
it('throws for empty string ""', () => {
expect(() => validateAgentName("", ROOT)).toThrow();
});
it('allows "my-agent"', () => {
expect(() => validateAgentName("my-agent", ROOT)).not.toThrow();
});
it('allows "agent-123"', () => {
expect(() => validateAgentName("agent-123", ROOT)).not.toThrow();
});
});