File Organizer processes user-supplied file paths and file contents from
the local filesystem. The primary attack surfaces are path traversal
(a caller-supplied path escaping the intended directory tree via ..
or a symlink) and resource exhaustion (oversized/malicious archive,
image, or document inputs). The codebase addresses these with three
primitives, all under src/file_organizer/utils/ and
src/file_organizer/core/:
SafeDir(utils/safedir.py) — the path-safety primitive.open_child()/open_for_reader()/open_subdir()/open_anchored_reader()/open_anchored_writer()/lstat()/mkdir()/unlink()/rename_into()reject any path component that would escape the rooted directory, including symlink-based escapes, and raiseValueErroron rejection. Call sites are expected to let thatValueErrorpropagate or handle it explicitly — see thesafedir-valueerrorlint rail below.atomic_write()/durable_move()(utils/atomic_write.py,undo/durable_move.py) — crash-safety primitives. Writes go through a temp-file-plus-os.replace()sequence so a crash mid-write never leaves a truncated file in place of a previously-good one; moves verify(st_dev, st_ino)identity afteros.replace()to detect a symlink swap mid-move.PidFileManager.claim_pid_file()(daemon/pid.py) — anO_CREAT|O_EXCLatomic claim closing the daemon-startup TOCTOU race between checking whether a PID file exists and writing one.
Web/API surfaces (src/file_organizer/web/, src/file_organizer/api/)
additionally rely on FastAPI/Starlette's built-in request validation,
CSRF middleware (web/csrf.py), and defusedxml for any XML parsing
(see the defusedxml-fallback rail below) to avoid XXE.
Static type checking (mypy, strict = true repo-wide) and linting
(ruff) run on every commit; see Lint Rails below for the
project-specific AST checks layered on top of both.
The rails below are AST-based static checks run via
scripts/ci/ci_rails.py (registry: scripts/ci/rails.toml),
either as a pre-commit hook or in CI. Rails with no current violations
are enforced; rails with pre-existing violation backlogs remain
advisory until their cleanup issues are resolved.
| Rail | What it flags | Status |
|---|---|---|
safedir-required |
Raw open()/Path.open()/shutil.copy*/shutil.move outside the SafeDir primitives themselves |
enforced |
atomic-write |
Raw file-write operations bypassing atomic_write() |
enforced |
cli-path-validation |
A CLI command's Path parameter not wrapped in resolve_cli_path() |
enforced |
cli-file-kind-validation |
A CLI path resolved with must_be_dir=False without a subsequent file/directory kind check |
advisory |
defusedxml-fallback |
Importing from stdlib xml instead of defusedxml |
enforced |
test-hardcoded-paths |
Hardcoded absolute paths in tests (use tmp_path) |
enforced |
test-separator-paths |
Hardcoded path separators in Path(...) calls in tests |
enforced |
pytest-raises-hygiene |
pytest.raises() without a match= regex on a generic/built-in exception |
enforced |
safedir-valueerror |
A broad except Exception/bare except around a SafeDir call that doesn't re-raise or catch ValueError explicitly |
enforced |
textiowrapper-detach |
An io.TextIOWrapper that is never .detach()-ed before going out of scope (use-after-close risk on the wrapped buffer/fd) |
enforced |
called-attribute-assertion |
Weak assert mock.called / bare assert mock.call_count test assertions |
enforced |
xdist-loadgroup |
A test using the xdist-wide tmp_path_factory.getbasetemp() without an xdist_group marker |
enforced |
Per-file coverage floors (check-integration-floors.py for the
integration suite, check_module_coverage_floor.py for the unit suite)
are wired as direct CI workflow steps in ci.yml — run immediately
after the pytest step that produces their input coverage JSON — rather
than through the rail registry above, since ci_rails.py has no
step-ordering concept and both checks depend on a coverage artifact
from an earlier step in the same job.
Lint/type config (separate from the AST rails above, enforced unconditionally, not advisory):
| Check | What it does |
|---|---|
ruff check (incl. PT012/PT017/PT022) |
Style, correctness, and pytest-hygiene lint, including pytest.raises()-block hygiene |
mypy src/file_organizer/ |
Strict type checking across the whole package (see Known Limitations for the ratchet exemption) |
Supply-chain scanning (run in .github/workflows/security.yml):
| Check | What it does | Status |
|---|---|---|
pip-audit |
Scans installed dependencies against the OSV database | enforced via scripts/security/pip_audit_gate.py, findings suppressible only via a documented entry in .github/accepted-risks.yml |
bandit |
Static analysis for common Python security anti-patterns | advisory (|| true) — see Known Limitations |
codeql-analysis |
GitHub's semantic code-scanning | enforced |
bandit -r src/is not gated. After triaging and annotating the non-cryptographic weak-hash call sites, the remaining findings are low/medium severity and still need review before this job can become blocking. Tracked as #1344.- mypy's
strict = trueconfig is repo-wide, but 41 files are exempted via a dated[[tool.mypy.overrides]] ignore_errors = trueratchet baseline (seepyproject.toml, "WP-6.4 ratchet baseline"). New files get full enforcement; the 41 existing ones are fixed incrementally — remove an entry as its file is cleaned up. - One lint rail above remains advisory (
cli-file-kind-validation). It is intentionally non-blocking while remediation and detector tuning continue prior to promotion to enforced.
We take the security of Local-File-Organizer seriously. If you believe you have found a security vulnerability in this project, please report it via a private GitHub Security Advisory on this repository rather than a public issue:
https://github.com/curdriceaurora/Local-File-Organizer/security/advisories/new
Please include:
- Type of vulnerability (e.g., path traversal, XXE, symlink race, etc.)
- Full details of the environment (OS, Python version, etc.)
- A detailed description of the vulnerability including steps to reproduce
- Any proof-of-concept code or screenshots
You should receive an acknowledgment of your report within 48 hours. We will do our best to triage and address the issue within a reasonable timeframe (typically 7-14 days). Once the issue is resolved and a patch is released, we will publicly acknowledge your contribution (if desired).
| Version | Supported |
|---|---|
| v2.0.x | ✅ |
| < 2.0 | ❌ |