fix(spawn): handle special characters in project names
- Reject null bytes and control characters (U+0000-U+001F, U+007F) in _validate_spawn_params with explicit INVALID_PROJECT error - Reject whitespace-only project names as MISSING_PROJECT - Reject non-string project names (int, list, etc.) - Add _sanitize_pane_name() to clean Zellij pane names: replaces quotes, backticks, and control chars with underscores; collapses whitespace; truncates to 64 chars - Add 44 new tests: safe chars (hyphens, spaces, dots, @, +, #), dangerous chars (null byte, newline, tab, ESC, DEL), shell metacharacters ($, ;, backtick, |), pane name sanitization, and spawn command construction with special char names Closes bd-14p
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
@@ -20,6 +21,20 @@ AGENT_COMMANDS = {
|
||||
# Module-level cache for projects list (AC-33)
|
||||
_projects_cache: list[str] = []
|
||||
|
||||
# Characters unsafe for Zellij pane/tab names: control chars, quotes, backticks
|
||||
_UNSAFE_PANE_CHARS = re.compile(r'[\x00-\x1f\x7f"\'`]')
|
||||
|
||||
|
||||
def _sanitize_pane_name(name):
|
||||
"""Sanitize a string for use as a Zellij pane name.
|
||||
|
||||
Replaces control characters and quotes with underscores, collapses runs
|
||||
of whitespace into a single space, and truncates to 64 chars.
|
||||
"""
|
||||
name = _UNSAFE_PANE_CHARS.sub('_', name)
|
||||
name = re.sub(r'\s+', ' ', name).strip()
|
||||
return name[:64] if name else 'unnamed'
|
||||
|
||||
|
||||
def load_projects_cache():
|
||||
"""Scan ~/projects/ and cache the list. Called on server start."""
|
||||
@@ -112,9 +127,17 @@ class SpawnMixin:
|
||||
|
||||
def _validate_spawn_params(self, project, agent_type):
|
||||
"""Validate spawn parameters. Returns resolved_path or error dict."""
|
||||
if not project:
|
||||
if not project or not isinstance(project, str):
|
||||
return {'error': 'Project name is required', 'code': 'MISSING_PROJECT'}
|
||||
|
||||
# Reject whitespace-only names
|
||||
if not project.strip():
|
||||
return {'error': 'Project name is required', 'code': 'MISSING_PROJECT'}
|
||||
|
||||
# Reject null bytes and control characters (U+0000-U+001F, U+007F)
|
||||
if '\x00' in project or re.search(r'[\x00-\x1f\x7f]', project):
|
||||
return {'error': 'Invalid project name', 'code': 'INVALID_PROJECT'}
|
||||
|
||||
# Reject path traversal characters (/, \, ..)
|
||||
if '/' in project or '\\' in project or '..' in project:
|
||||
return {'error': 'Invalid project name', 'code': 'INVALID_PROJECT'}
|
||||
@@ -221,7 +244,7 @@ class SpawnMixin:
|
||||
|
||||
# Build agent command
|
||||
agent_cmd = AGENT_COMMANDS[agent_type]
|
||||
pane_name = f'{agent_type}-{project}'
|
||||
pane_name = _sanitize_pane_name(f'{agent_type}-{project}')
|
||||
|
||||
# Spawn pane with agent command
|
||||
env = os.environ.copy()
|
||||
|
||||
Reference in New Issue
Block a user