Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Sentinel Journal

## 2025-12-20 - [Log Injection Prevention]
**Vulnerability:** Agent instance IDs extracted from user prompts were not validated, allowing potential injection of path traversal characters or scripts into logs.
**Learning:** Parsing metadata from unstructured text (prompts) is risky without strict validation, as prompts are fully user-controlled.
**Prevention:** Implemented strict allowlist validation (`^[a-zA-Z0-9\-_]+$`) for all extracted IDs before using them.
38 changes: 29 additions & 9 deletions src/lib/metadata-extraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ export interface AgentInstanceMetadata {
parent_task_id?: string; // Task ID that spawned this agent
}

/**
* Validate that an ID string contains only safe characters
* Allows alphanumeric, hyphens, and underscores.
* Prevents path traversal and injection attacks.
*/
function isValidId(id: string): boolean {
return /^[a-zA-Z0-9\-_]+$/.test(id);
}

/**
* Extract agent instance ID from Task tool input
*
Expand Down Expand Up @@ -52,13 +61,18 @@ export function extractAgentInstanceId(
if (!result.agent_instance_id && toolInput?.prompt && typeof toolInput.prompt === 'string') {
const promptMatch = toolInput.prompt.match(/\[AGENT_INSTANCE:\s*([^\]]+)\]/);
if (promptMatch) {
result.agent_instance_id = promptMatch[1].trim();

// Parse agent type and instance number from ID
const parts = result.agent_instance_id!.match(/^([a-z-]+)-(\d+)$/);
if (parts) {
result.agent_type = parts[1];
result.instance_number = parseInt(parts[2], 10);
const extractedId = promptMatch[1].trim();

// Security: Validate ID format to prevent injection
if (isValidId(extractedId)) {
result.agent_instance_id = extractedId;

// Parse agent type and instance number from ID
const parts = result.agent_instance_id!.match(/^([a-z-]+)-(\d+)$/);
if (parts) {
result.agent_type = parts[1];
result.instance_number = parseInt(parts[2], 10);
}
}
}
}
Expand All @@ -68,14 +82,20 @@ export function extractAgentInstanceId(
if (toolInput?.prompt && typeof toolInput.prompt === 'string') {
const parentSessionMatch = toolInput.prompt.match(/\[PARENT_SESSION:\s*([^\]]+)\]/);
if (parentSessionMatch) {
result.parent_session_id = parentSessionMatch[1].trim();
const extractedId = parentSessionMatch[1].trim();
if (isValidId(extractedId)) {
result.parent_session_id = extractedId;
}
}

// Extract parent task from prompt
// Example: "[PARENT_TASK: research_1731445892345]"
const parentTaskMatch = toolInput.prompt.match(/\[PARENT_TASK:\s*([^\]]+)\]/);
if (parentTaskMatch) {
result.parent_task_id = parentTaskMatch[1].trim();
const extractedId = parentTaskMatch[1].trim();
if (isValidId(extractedId)) {
result.parent_task_id = extractedId;
}
}
}

Expand Down