|
| 1 | +# Security Policy |
| 2 | + |
| 3 | +## Supported Versions |
| 4 | + |
| 5 | +OpenCut ships rapidly. We actively support the **latest minor** (`1.33.x`) and the one immediately preceding it (`1.32.x`). Older minors receive security-only backports for 90 days after they're superseded. |
| 6 | + |
| 7 | +| Version | Supported | Security fixes until | |
| 8 | +|---------|-------------------|----------------------| |
| 9 | +| 1.33.x | ✅ Active | — | |
| 10 | +| 1.32.x | ✅ Previous | +90 days after 1.33 | |
| 11 | +| 1.31.x | ⚠️ Critical only | +30 days after 1.33 | |
| 12 | +| ≤ 1.30 | ❌ End of life | n/a | |
| 13 | + |
| 14 | +Version numbers ship in [`opencut/__init__.py`](opencut/__init__.py) and are kept in sync by [`scripts/sync_version.py`](scripts/sync_version.py). |
| 15 | + |
| 16 | +## Reporting a Vulnerability |
| 17 | + |
| 18 | +**Please do not open public GitHub issues for security problems.** |
| 19 | + |
| 20 | +Email [matt@mavenimaging.com](mailto:matt@mavenimaging.com) with: |
| 21 | + |
| 22 | +1. A description of the issue — what you see, what you expected. |
| 23 | +2. Reproducer steps, ideally as a minimal request / script / config. |
| 24 | +3. The commit SHA + `__version__` you tested against. |
| 25 | +4. Your assessment of severity (low / medium / high / critical) and why. |
| 26 | + |
| 27 | +We acknowledge reports within **72 hours** and aim to land a fix or mitigation within: |
| 28 | + |
| 29 | +- **Critical** — 72 hours (RCE, auth bypass, data exfiltration, sandbox escape) |
| 30 | +- **High** — 7 days (privilege escalation, unauthenticated DoS on a production endpoint) |
| 31 | +- **Medium** — 30 days (authenticated DoS, information disclosure, supply-chain risk) |
| 32 | +- **Low** — 90 days (hardening suggestions, theoretical concerns) |
| 33 | + |
| 34 | +We don't run a paid bounty programme, but we credit reporters in `CHANGELOG.md` and the release notes unless you prefer to remain anonymous. |
| 35 | + |
| 36 | +## In Scope |
| 37 | + |
| 38 | +- `opencut/` backend (Flask API, job system, CLI, MCP server) |
| 39 | +- `extension/com.opencut.panel/` CEP panel (HTML/JS/ExtendScript) |
| 40 | +- `extension/com.opencut.uxp/` UXP panel (HTML/JS) |
| 41 | +- `installer/` (C# WPF Windows installer) |
| 42 | +- `scripts/` build + utility scripts |
| 43 | +- `tests/fuzz/` harness targets |
| 44 | + |
| 45 | +## Out of Scope |
| 46 | + |
| 47 | +- Third-party dependencies (report upstream). We monitor `pyproject.toml` / `requirements.txt` via Dependabot. |
| 48 | +- User-supplied plugins loaded via `~/.opencut/plugins/` — plugins run with the host's trust, so audit before installing. |
| 49 | +- Social-engineering / phishing attacks against maintainers. |
| 50 | +- Reports that require pre-existing local code execution (e.g. "an attacker with shell access can edit `~/.opencut/settings.json`"). |
| 51 | + |
| 52 | +## Known-Safe-By-Design Surface |
| 53 | + |
| 54 | +OpenCut's security model leans on a handful of intentional choices: |
| 55 | + |
| 56 | +- **CSRF on every mutation.** `@require_csrf` decorator on all `POST`/`PUT`/`PATCH`/`DELETE` routes. Token rotates per server start, delivered via `GET /health`, sent as `X-OpenCut-Token` header. |
| 57 | +- **Path validation.** All file-accepting routes pass user-supplied paths through `security.validate_path()` / `validate_filepath()` / `validate_output_path()`. Realpath resolution, null-byte rejection, symlink-out-of-allowlist defence. |
| 58 | +- **SSRF defence.** Outbound URL validators (`_validate_webhook_url`, `_validate_download_url`) reject localhost, loopback, private IPs, link-local, reserved ranges. |
| 59 | +- **Rate-limit categories.** Four-way classification (`gpu_heavy` / `cpu_heavy` / `io_bound` / `light`) bounds concurrent work per category — see `core/rate_limit_categories.py`. |
| 60 | +- **Scripting console sandbox.** Dunder builtins stripped, `__import__` / `exec` / `eval` / `compile` / `open` / `os` / `sys` / `subprocess` blocked in AST. Context keys containing `__` rejected. |
| 61 | +- **Fuzz harness** for parsers (`tests/fuzz/`) — SRT / VTT / `.cube` / voice-grammar parsers are expected to be total. |
| 62 | +- **Atomic writes** for user-data files via `tempfile + os.replace`. |
| 63 | + |
| 64 | +## Hardening Recommendations |
| 65 | + |
| 66 | +Operators running OpenCut in a shared-network environment should: |
| 67 | + |
| 68 | +1. Bind to `127.0.0.1` only (default) — the service is single-user. Non-loopback binds require `OPENCUT_ALLOW_REMOTE=1`. |
| 69 | +2. **Use the persistent local auth token when binding non-loopback.** |
| 70 | + Setting `OPENCUT_ALLOW_REMOTE=1` automatically issues a token under |
| 71 | + `~/.opencut/auth.json` (POSIX: mode `0600`). Every non-loopback |
| 72 | + request must include `X-OpenCut-Auth: <token>`. Loopback peers |
| 73 | + (`127.0.0.1`, `::1`) still bypass the token to keep the single-user |
| 74 | + workflow snappy. |
| 75 | +3. Read or rotate the token explicitly: |
| 76 | + ```bash |
| 77 | + opencut-server --print-auth # print the persisted token |
| 78 | + opencut-server --rotate-auth # generate a fresh token, then exit |
| 79 | + ``` |
| 80 | + The `GET /auth/info` endpoint returns *metadata only* — it never |
| 81 | + includes the token value. Treat `~/.opencut/auth.json` like an SSH |
| 82 | + private key; never check it into source control. |
| 83 | +4. Set `SENTRY_DSN` so crashes route to a tracker you control. |
| 84 | +5. Set `PLAUSIBLE_HOST` + `PLAUSIBLE_DOMAIN` (optional) for usage telemetry. |
| 85 | +6. Configure `OPENCUT_TEMP_CLEANUP_*` to fit the expected workload. |
| 86 | +7. Use the bundled FFmpeg or build FFmpeg explicitly — distro builds can lag on CVE fixes. |
| 87 | +8. Keep `~/.opencut/plugins/` empty until you've audited each plugin manifest. |
| 88 | + |
| 89 | +### Threat model for non-loopback binds |
| 90 | + |
| 91 | +Default deployment: a single user, on their workstation, talking to |
| 92 | +`127.0.0.1:5679`. The Adobe Premiere CEP/UXP panel sits on the same |
| 93 | +machine. We deliberately do **not** require an API key in that path |
| 94 | +because the token would be visible to anything that can read the panel |
| 95 | +preferences anyway. |
| 96 | + |
| 97 | +When the operator opts into `OPENCUT_ALLOW_REMOTE=1` (e.g. remote |
| 98 | +render host on a private VLAN), the threat surface changes: |
| 99 | + |
| 100 | +- Anyone who can hit the bind address can issue render jobs, read media |
| 101 | + paths, or call shell-adjacent endpoints (FFmpeg invocations, OS |
| 102 | + shell-out for ``open``/Finder integration). |
| 103 | +- CSRF alone is not enough — CSRF protects browser sessions, not API |
| 104 | + clients on the same network. |
| 105 | + |
| 106 | +The local auth token closes that gap: non-loopback callers must include |
| 107 | +the token in the `X-OpenCut-Auth` header (a `?auth=` query string is |
| 108 | +also accepted for tools that can't set headers). `/health` and |
| 109 | +`/auth/info` remain exempt so panels can bootstrap connectivity and |
| 110 | +render a "Authentication required" hint. |
| 111 | + |
| 112 | +## Software Bill of Materials (SBOM) |
| 113 | + |
| 114 | +Generate a declared-dependency CycloneDX SBOM from `pyproject.toml`, |
| 115 | +`requirements.txt`, and OpenCut model-card metadata: |
| 116 | + |
| 117 | +```bash |
| 118 | +python scripts/sbom.py |
| 119 | +``` |
| 120 | + |
| 121 | +The script writes `dist/opencut-declared-sbom.cyclonedx.json` (or `.xml` with |
| 122 | +`--format xml`) and marks the CycloneDX metadata as `declared-only`. The |
| 123 | +committed `requirements-lock.txt` is audited separately by the pip-audit release |
| 124 | +gate; it is not presented as a resolved installed-package SBOM inventory. |
0 commit comments