|
11 | 11 | import traceback |
12 | 12 | from datetime import UTC, datetime |
13 | 13 | from pathlib import Path |
| 14 | +import re |
14 | 15 |
|
15 | 16 | from arctrl import ARC |
16 | 17 | from arctrl.py.fable_modules.fable_library.async_ import start_as_task # type: ignore[import-untyped] |
@@ -122,13 +123,24 @@ def _fallback() -> tuple[str, Path]: |
122 | 123 | target = (base_resolved / rid).resolve() |
123 | 124 | return rid, target |
124 | 125 |
|
| 126 | + # Allow only simple, short directory names consisting of safe characters. |
| 127 | + # This ensures that user-controlled identifiers cannot introduce path |
| 128 | + # traversal or unexpected filesystem semantics. |
| 129 | + safe_name_pattern = re.compile(r"^[A-Za-z0-9_.-]{1,64}$") |
| 130 | + |
125 | 131 | if isinstance(raw_id, str) and raw_id.strip(): |
126 | 132 | candidate_id = raw_id.strip() |
127 | 133 | # Reduce to a single path component and normalize it. |
128 | 134 | safe_name = os.path.normpath(Path(candidate_id).name) |
129 | | - # Reject empty names, current/parent directory markers, or anything that |
130 | | - # would reintroduce directory components on this platform. |
131 | | - if not safe_name or safe_name in {".", ".."} or "/" in safe_name or "\\" in safe_name: |
| 135 | + # Reject empty names, current/parent directory markers, any embedded |
| 136 | + # separators, or names that do not match the allowed pattern. |
| 137 | + if ( |
| 138 | + not safe_name |
| 139 | + or safe_name in {".", ".."} |
| 140 | + or "/" in safe_name |
| 141 | + or "\\" in safe_name |
| 142 | + or not safe_name_pattern.match(safe_name) |
| 143 | + ): |
132 | 144 | arc_id, candidate_dir = _fallback() |
133 | 145 | else: |
134 | 146 | candidate_dir = (base_resolved / safe_name).resolve() |
|
0 commit comments