Repository navigation
[bug-fix] Harden persisted workflow run artifact access against symlinks - #4930
github-actions[bot] wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Snapshot reads retain a symlink race, Ruff fails on an unused variable, and key consumer regressions remain untested.
4 open findings
What changed in this PR
Adds a shared security boundary for persisted workflow-run artifacts to reject symlink redirection.
Changes:
- Validates run directories and artifacts before access.
- Adds no-follow file opening and CLI error handling.
- Documents and tests the symlink policy.
| File | Description |
|---|---|
src/specify_cli/workflows/engine.py |
Implements storage validation and protected artifact access. |
src/specify_cli/workflows/command_status.py |
Handles unsafe listing failures. |
tests/specify_cli/workflows/test_run_state_storage.py |
Adds symlink rejection tests. |
workflows/README.md |
Documents user-facing policy. |
workflows/ARCHITECTURE.md |
Documents the storage boundary. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| err = cli._error_console(json_output) | ||
| try: | ||
| runs = engine.list_runs() | ||
| except (OSError, ValueError) as exc: | ||
| err.print(f"[red]Error:[/red] {cli._escape_markup(str(exc))}") |
There was a problem hiding this comment.
Added workflow status --json regressions for symlinked run directories and state.json, asserting clean failure and stderr-only output. Fixed in 79f1da7.
There was a problem hiding this comment.
The workflow status --json listing regressions for symlinked run directories and state.json are present and pass on the current branch (02159bf0).
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
| if state_path.exists(): | ||
| try: | ||
| with open(state_path, encoding="utf-8") as f: | ||
| with self._open_artifact(state_path, "r", encoding="utf-8") as f: |
There was a problem hiding this comment.
The unreadable-state regression now patches RunState._open_artifact and still asserts that the bad row is skipped; it passes on the current branch (02159bf0).
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
| directory_flag is None | ||
| or nofollow is None | ||
| or os.open not in os.supports_dir_fd | ||
| ): | ||
| raise OSError("Secure workflow run-directory access is unavailable") |
There was a problem hiding this comment.
Added a checked path-based fallback for platforms without directory-descriptor/no-follow support, including execute/resume/listing and symlink regressions. Committed as 02159bf0; the architecture note documents that this fallback cannot prevent concurrent path replacement.
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
| RunState._open_artifact( | ||
| artifact, "r", dir_fd=run_fd, encoding="utf-8" | ||
| ) |
| try: | ||
| RunState._validate_fallback_path(tmp_path) | ||
| tmp_path.unlink() | ||
| except (OSError, ValueError): |


Bug fix — persisted-run-symlinks
Proposed fix for issue #4914, applying the remediation from the bug assessment.
Verdict: valid · Severity: medium
Summary
Introduces a shared persisted-run storage boundary that rejects symlinked run directories and artifact files before workflow state, input, snapshot, or log access. Resume, status, run listing, and snapshot creation now fail closed consistently, with the policy documented for workflow users and maintainers.
Changes
src/specify_cli/workflows/engine.pysrc/specify_cli/workflows/command_status.pytests/specify_cli/workflows/test_run_state_storage.pyworkflows/README.mdworkflows/ARCHITECTURE.mdTests Added or Updated
tests/specify_cli/workflows/test_run_state_storage.py::test_load_rejects_symlinked_run_directory— rejects redirected per-run directories.tests/specify_cli/workflows/test_run_state_storage.py::test_run_state_rejects_symlinked_persisted_artifacts— rejects redirected state and input artifacts.tests/specify_cli/workflows/test_run_state_storage.py::test_run_state_rejects_symlinked_artifact_writes— rejects redirected state, input, and log writes.tests/specify_cli/workflows/test_run_state_storage.py::test_workflow_snapshot_artifact_is_checked— rejects redirected workflow snapshots.Local Verification
python3 -m compileall -q src/specify_cli/workflows/engine.py src/specify_cli/workflows/command_status.py tests/specify_cli/workflows/test_run_state_storage.py→ passed.git diff --check→ passed.pytestis unavailable in the environment; no dependencies were installed.Deviations from Assessment
workflow definitioncommand, so no command-specific change was possible there; persisted snapshot access is covered in the shared engine layer.O_NOFOLLOWwhere available plus immediate symlink checks; no concurrent mutation test was added because the targeted test runner is unavailable.Risks & Review Notes
Refs #4914 · cc
@markuswondrak