roost is pre-1.0; only the latest released version receives security fixes.
| Version | Supported |
|---|---|
latest 0.x |
✅ |
| older | ❌ |
Please do not open a public issue for security problems.
Use GitHub Private Vulnerability Reporting (the repo's Security → Report a vulnerability
tab), or email rn.wolfe@gmail.com with roost security in the subject.
- Acknowledgement: within ~48 hours.
- Disclosure: coordinated. We'll agree on a timeline and credit you (opt-in) in the release notes.
- Safe harbor: good-faith research that respects others' privacy/data and avoids service
disruption will not be pursued. Include a minimal reproducible PoC and the
roost versionoutput.
roost is a read-only, no-auth tool. It never logs in, never books, and stores no user
secrets or credentials — there is no login, no OAuth token, no API key of yours, and no cookie
jar. The classic credential-handling attack surface (at-rest secret storage, argv/ps leakage,
keyring fallback) therefore does not apply. The relevant surface is narrower:
| Threat | Mitigation |
|---|---|
| SSRF / local-file read via the update check | roost version --check will follow a ROOST_RELEASES_URL override only over https:// (any host) or http:// to localhost/127.0.0.1/::1 (for tests). A hostile value (file://..., http://169.254.169.254/..., other schemes/hosts) is ignored and the check falls back to the default endpoint — the override can't be turned into an SSRF or local-file read. Enforced by _safe_release_url() and pinned by tests/test_cli.py::test_version_check_rejects_unsafe_scheme. |
| Prompt injection via third-party property text | Free text from the upstream (property names, deal blurbs, amenity strings, review snippets, OTA/source names) is fenced/sanitized as untrusted by default in agent mode (--wrap-untrusted): treated as data, not instructions. Un-fencing requires an explicit --no-wrap-untrusted on the human path. |
| Upstream tampering / breakage | The reverse-engineered GraphQL backend is treated as untrusted. A failed public-key/persisted-hash re-scrape or a changed response shape becomes UPSTREAM_DRIFT (exit 20) — distinct from a throttle/block (exit 7) — never silent wrong data. |
| Backend etiquette / legitimacy | roost reads a path Google's robots.txt does not disallow (/travel/hotels/...), enforced as an allowlist at the network boundary, and presents the real web client's TLS profile at low volume. It performs no evasion: no proxy/IP rotation, no residential proxies, no CAPTCHA solving, no session replay. It will not silently fall back to a fingerprint it did not choose — an unavailable pinned fingerprint is a hard error, not a degrade. On a block it circuit-breaks and stops (exit 20) rather than retrying into it. The rule is reduce volume, never disguise identity; if Google blocks it, the correct response is to stop. |
$XDG_STATE_HOME/roost/ (typically ~/.local/state/roost/) holds only non-secret data:
- No target credentials at all on the default backend —
roostsends no key, no cookie, and no login. The only secret it can hold is an optional SerpApi key for the alternate backend, stored in the OS keyring (0600 file fallback) and never accepted via argv. cached with a TTL. - Throttle token-bucket / last-request timestamps and circuit-breaker state (
tripped_until).
None of this is a user credential; deleting the directory is always safe (it re-bootstraps on the next call).