You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Address review: gate ALL scaffolding entry points + robust name validation
Adversarial review found the first cut was incomplete (two blocking issues):
- `new` (cmd_new) is another user-name scaffolding entry point that was NOT guarded, so
`specsync new "../../PWNED" --full` still escaped the project on every platform.
- The blocklist relied on `is_absolute()`, which misses Windows drive-relative names
(`C:foo`): no separator, not absolute, so it passed — and on Windows `join` replaces
the base, escaping `<specs_dir>/<name>/`.
Plus two follow-ups it surfaced: `wizard` had the same traversal gap (interactive, only
checked empty), and control characters/newlines were accepted (YAML frontmatter
injection / control-char dir names, in-project but unexpected).
Reworked into a single shared `validate_module_name` in commands/mod.rs (next to
`load_and_discover`) and gated ALL FOUR scaffolding entry points: `new`, `add-spec`,
`scaffold`, `wizard`. The validator now requires a single `Component::Normal` segment
with no raw separator and no control chars — platform-aware, so it also rejects Windows
drive-relative prefixes that `is_absolute()` misses, and blocks frontmatter injection.
Reproduced: `new "../../PWNED" --full` went from "files written outside project, exit 0"
to `invalid module name … / exit 1` with nothing created outside; a newline-injecting
name is refused.
Tests: `validate_module_name` unit tests (plain/unicode names ok; empty, separators,
`.`/`..`, absolute, control chars rejected; `#[cfg(windows)]` drive-relative rejected);
`scaffold_rejects_module_name_path_traversal` now also covers `new`. Documented the
shared export in commands.spec.md and the new error case in the new/wizard specs.
734 unit + 170 integration, fmt / clippy (bin) / self-check 100% (37851 LOC).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDJxU4R8hUEuq1Y5jzft5m
Copy file name to clipboardExpand all lines: specs/cmd_new/cmd_new.spec.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -55,6 +55,7 @@ Implements the `specsync new` command. Quick-creates a minimal spec with auto-de
55
55
| Spec already exists | Exits 1 |
56
56
| No source files found | Creates spec with empty `files:` and prints a ⚠ explaining that the `files:` list must be filled in before `check` passes |
57
57
| Dir creation fails | Exits 1 |
58
+
| Invalid module name (path separator, `.`/`..`, absolute/drive-relative, control chars) | Refused via `validate_module_name` before any write; prints `invalid module name …` and exits 1 (no path traversal) |
Copy file name to clipboardExpand all lines: specs/cmd_wizard/cmd_wizard.spec.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -54,7 +54,7 @@ Implements the `specsync wizard` command — an interactive TUI wizard for creat
54
54
55
55
| Condition | Behavior |
56
56
|-----------|----------|
57
-
| Empty module name entered | Exits with code 1|
57
+
| Empty or unsafe module name entered (path separator, `.`/`..`, absolute/drive-relative, control chars) | Refused via `validate_module_name`; prints `invalid module name …` and exits 1 (no path traversal)|
58
58
| Spec directory already exists | Prints error and exits 1 |
59
59
| User cancels at confirmation | Exits cleanly with code 0 |
Copy file name to clipboardExpand all lines: specs/commands/commands.spec.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,6 +29,7 @@ Shared command infrastructure used by all CLI subcommands. Provides config loadi
29
29
| Function | Parameters | Returns | Description |
30
30
|----------|-----------|---------|-------------|
31
31
|`load_and_discover`|`root: &Path, allow_empty: bool`|`(SpecSyncConfig, Vec<PathBuf>)`| Load config and discover all spec files (excluding `_`-prefixed); exits if empty and `allow_empty` is false |
32
+
|`validate_module_name`|`module_name: &str`|`Result<(), String>`| Validate a user-supplied module name for the scaffolding commands (`new`, `add-spec`, `scaffold`, `wizard`): must be a single plain path segment (one `Component::Normal`, no separators/`.`/`..`/absolute/drive-relative/control chars), preventing path traversal outside the project |
32
33
|`filter_specs`|`root: &Path, spec_files: &[PathBuf], filters: &[String]`|`Vec<PathBuf>`| Filter spec files by user-provided names/paths (exact path, relative path, filename, module name); returns all if filters is empty |
33
34
|`filter_by_status`|`spec_files: &[PathBuf], exclude: &[String], only: &[String]`|`Vec<PathBuf>`| Filter spec files by their frontmatter status field; supports exclude-list and allow-list modes |
34
35
|`build_schema_columns`|`root: &Path, config: &SpecSyncConfig`|`HashMap<String, SchemaTable>`| Build column-level schema from migration files if `schema_dir` is configured |
0 commit comments