fix(security): replace wildcard CORS on core RPC with origin allowlist#2266
Conversation
The in-process JSON-RPC server returned `Access-Control-Allow-Origin: *` on every response, meaning any browser origin in possession of the bearer token could make authenticated cross-origin requests against `/rpc`. Replaces the wildcard with an explicit allowlist: - Tauri v2 webview origins (`tauri://localhost`, `http(s)://tauri.localhost`) - Loopback hosts on any port (`http://127.0.0.1:*`, `http://localhost:*`, `http://[::1]:*`) for the Vite dev server and E2E harnesses - Comma-separated env override `OPENHUMAN_CORE_ALLOWED_ORIGINS` for operator-controlled debug harnesses Disallowed origins receive no ACAO header (browser blocks). Non-browser callers (no `Origin` header) are unaffected. `Vary: Origin` is set so intermediate caches keep per-origin responses distinct. Closes tinyhumansai#2262
📝 WalkthroughWalkthroughReplaces wildcard CORS with explicit origin allowlisting for Tauri and loopback origins. Introduces ChangesCORS Allowlist and Origin Validation
Sequence Diagramflowchart LR
A["HTTP Request<br/>with Origin header"] --> B["Extract Origin<br/>from request"]
B --> C["is_origin_allowed<br/>checks:<br/>• Tauri origins<br/>• Loopback 127.0.0.1<br/>• Loopback localhost<br/>• Loopback ::1<br/>• ENV override"]
C -->|Origin allowed| D["Response:<br/>Access-Control-Allow-Origin: origin<br/>Vary: Origin<br/>CORS headers"]
C -->|Origin denied| E["Response:<br/>No Allow-Origin<br/>Vary: Origin<br/>CORS headers<br/>warn log"]
C -->|No Origin header| F["Response:<br/>No Allow-Origin<br/>Vary: Origin<br/>CORS headers"]
D --> G["Browser allows<br/>cross-origin use"]
E --> H["Browser blocks<br/>cross-origin use"]
F --> I["Non-browser or<br/>same-origin unaffected"]
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested Labels
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/core/jsonrpc_cors_tests.rs`:
- Around line 80-103: The test env_override_allows_extra_origins unsafely
mutates process-global env via unsafe set_var/remove_var; instead stop touching
the environment and either (A) call the pure helper that parses the
ALLOWED_ORIGINS value directly (e.g., add/use a function like
parse_allowed_origins(allowlist: &str) and test is_origin_allowed against that
parsed set) or (B) serialize the test with the serial_test crate by annotating
the test with #[serial] (and remove unsafe env mutations), or (C) use an
env-locking utility to safely set/remove ALLOWED_ORIGINS_ENV; update the test to
reference ALLOWED_ORIGINS_ENV and is_origin_allowed (or the new parse helper)
accordingly so no unsafe std::env::set_var/remove_var is used.
In `@src/core/jsonrpc.rs`:
- Around line 687-689: with_cors_headers currently overwrites any existing Vary
header; update the function (with_cors_headers) to preserve and append "Origin"
instead of replacing it: read headers.get(header::VARY), if present combine the
existing value and "Origin" (comma-separated) while avoiding duplicate "Origin",
otherwise insert HeaderValue::from_static("Origin"); then set the resulting
header back into headers_mut() so inner responses keep their original Vary
contract.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a1f54936-5130-47ab-8b31-fb909fd6ff02
📒 Files selected for processing (2)
src/core/jsonrpc.rssrc/core/jsonrpc_cors_tests.rs
| fn env_override_allows_extra_origins() { | ||
| // SAFETY: this test mutates a process-global env var. No other test in | ||
| // this crate reads ALLOWED_ORIGINS_ENV, so parallel runs are safe; we | ||
| // still restore the previous value on exit to be a good citizen. | ||
| let prev = std::env::var(ALLOWED_ORIGINS_ENV).ok(); | ||
| unsafe { | ||
| std::env::set_var( | ||
| ALLOWED_ORIGINS_ENV, | ||
| "https://debug.internal, http://harness:9000", | ||
| ); | ||
| } | ||
|
|
||
| assert!(is_origin_allowed("https://debug.internal")); | ||
| assert!(is_origin_allowed("http://harness:9000")); | ||
| assert!(!is_origin_allowed( | ||
| "https://debug.internal.attacker.example" | ||
| )); | ||
|
|
||
| unsafe { | ||
| match prev { | ||
| Some(v) => std::env::set_var(ALLOWED_ORIGINS_ENV, v), | ||
| None => std::env::remove_var(ALLOWED_ORIGINS_ENV), | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What are the safety requirements for Rust's std::env::set_varandstd::env::remove_var, and are they sound to call inside parallel unit tests that may read environment variables concurrently?
💡 Result:
The functions std::env::set_var and std::env::remove_var are inherently unsafe to use in multi-threaded programs on most operating systems (specifically Unix-like systems) [1][2][3]. Safety Requirements: These functions are safe to call only in single-threaded programs or on Windows, where the operating system provides thread-safe environment manipulation [1][2][3]. On other platforms, they are considered unsafe because the underlying C library functions (such as setenv and unsetenv) are not thread-safe [4][5]. Because the environment is a global mutable state for the entire process, any concurrent read or write to the environment—even by other parts of the standard library or external C code—can result in undefined behavior [1][6][3]. It is practically impossible to guarantee that no other thread is reading the environment, as many functions (including those for DNS lookups or other system interactions) may access it internally without your knowledge [1][2][3]. Soundness in Parallel Unit Tests: It is generally not sound to call std::env::set_var or std::env::remove_var in parallel unit tests [7][5]. Because Rust's test runner executes tests in parallel by default, multiple tests may attempt to modify or read the environment simultaneously, leading to data races and undefined behavior [7][8]. To safely test code that relies on environment variables, you should: 1. Run tests serially: You can force tests to run one at a time using the --test-threads=1 flag with cargo test [9]. 2. Use synchronization: Use a crate like env-lock, which provides a mutex-based mechanism to ensure that only one test can modify or access the environment at a time [10][8]. 3. Avoid environment variables: If possible, refactor your code to accept configuration via arguments or dependency injection rather than relying on global environment variables [1][5].
Citations:
- 1: https://doc.rust-lang.org/std/env/fn.set_var.html
- 2: https://public-docs.ferrocene.dev/main/std/env/fn.set_var.html
- 3: https://doc.rust-lang.org/std/env/fn.remove_var.html
- 4: https://users.rust-lang.org/t/why-env-set-var-unsafe-now/134233
- 5: https://doc.rust-lang.org/edition-guide/rust-2024/newly-unsafe-functions.html
- 6: Consider deprecating and/or modifying behavior of std::env::set_var rust-lang/rust#90308
- 7:
std::env::{set_var, remove_var}is called in tests without safety documentation rust-lang/rust#148432 - 8: https://crates.io/crates/env-lock
- 9: https://users.rust-lang.org/t/passing-test-threads-1-to-cargo-test-by-default/87225
- 10: https://docs.rs/env-lock/latest/env_lock
Remove unsafe environment mutation from parallel unit test.
The unsafe { set_var/remove_var } calls are unsound in a parallel test harness. Rust's default test runner executes tests concurrently, and on Unix-like systems, the underlying C library functions for environment manipulation are not thread-safe. Even if no other test in this crate intentionally reads ALLOWED_ORIGINS_ENV, the environment is process-global state—other parts of the standard library, external dependencies, or system code may access it concurrently, leading to undefined behavior.
Refactor to either: (1) extract a pure helper that accepts the allowlist as a parameter and test that directly without env mutation, (2) use a synchronization crate like env-lock to serialize access, or (3) mark this test with #[serial] and run it serially (requires serial_test crate).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/core/jsonrpc_cors_tests.rs` around lines 80 - 103, The test
env_override_allows_extra_origins unsafely mutates process-global env via unsafe
set_var/remove_var; instead stop touching the environment and either (A) call
the pure helper that parses the ALLOWED_ORIGINS value directly (e.g., add/use a
function like parse_allowed_origins(allowlist: &str) and test is_origin_allowed
against that parsed set) or (B) serialize the test with the serial_test crate by
annotating the test with #[serial] (and remove unsafe env mutations), or (C) use
an env-locking utility to safely set/remove ALLOWED_ORIGINS_ENV; update the test
to reference ALLOWED_ORIGINS_ENV and is_origin_allowed (or the new parse helper)
accordingly so no unsafe std::env::set_var/remove_var is used.
| pub(super) fn with_cors_headers(mut response: Response, origin: Option<&str>) -> Response { | ||
| let headers = response.headers_mut(); | ||
| headers.insert( | ||
| header::ACCESS_CONTROL_ALLOW_ORIGIN, | ||
| HeaderValue::from_static("*"), | ||
| ); | ||
| headers.insert(header::VARY, HeaderValue::from_static("Origin")); |
There was a problem hiding this comment.
Preserve any existing Vary values.
Line 689 overwrites the whole Vary header, so an inner response that already set Vary loses that cache contract. Please append Origin instead of replacing the header.
💡 Minimal fix
- headers.insert(header::VARY, HeaderValue::from_static("Origin"));
+ headers.append(header::VARY, HeaderValue::from_static("Origin"));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/core/jsonrpc.rs` around lines 687 - 689, with_cors_headers currently
overwrites any existing Vary header; update the function (with_cors_headers) to
preserve and append "Origin" instead of replacing it: read
headers.get(header::VARY), if present combine the existing value and "Origin"
(comma-separated) while avoiding duplicate "Origin", otherwise insert
HeaderValue::from_static("Origin"); then set the resulting header back into
headers_mut() so inner responses keep their original Vary contract.
|
Thanks for the CORS allowlist fix! Two test-safety issues in 1. Race condition in parallel test runs
Fix: add a module-level 2. The test verifies Fix: add an assertion for Both fixes are implemented in |
|
I could not push directly to
Local validation on #2328 head cargo fmt --manifest-path Cargo.toml --all
GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml cors_tests --lib
GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml --lib
git diff --checkFocused CORS tests: 8 passed. Maintainers can either review/merge #2328 or cherry-pick |
|
huge welcome @leighstillard and massive thanks for landing your first one here 🙌 swapping the wildcard cors for a real origin allowlist on the rpc server is exactly the kind of hardening we love to see. you've earned a spot in our builder discord, come hang with us at https://discord.tinyhumans.ai/ ✨ |
|
Cheers I'm already there as primax. or primax#1590. @senamakel - is there a roadmap or contributor forum anywhere? I have a few features I want to add but they should be discussed first. |
## Summary - Replaces wildcard Core RPC CORS behavior with an explicit allowlist for Tauri and loopback origins. - Preserves existing `Vary` response values while appending `Origin`. - Removes unsafe process-global env mutation from CORS tests by injecting env override input directly. - Adds regression coverage for env override exact matching and existing `Vary` preservation. ## Problem - `src/core/jsonrpc.rs` emitted `Access-Control-Allow-Origin: *`, so any browser origin that obtained the bearer token could call the local RPC surface. - The prior fix in #2266 addressed the core issue but still had two review blockers: unsafe env mutation in parallel Rust tests and overwriting existing `Vary` headers. - I could not push directly to #2266's fork branch, so this PR carries the same security fix plus the review follow-ups. ## Solution - Keep `is_origin_allowed(origin)` as the production env-reading entry point. - Add `is_origin_allowed_with_extra(origin, extra_origins)` so tests can exercise override parsing without mutating process-global environment. - Change `with_cors_headers` from `headers.insert(Vary, Origin)` to `headers.append(Vary, Origin)`. - Add focused tests for existing `Vary` preservation and exact-match override behavior. ## Submission Checklist - [x] Tests added or updated (happy path + at least one failure / edge case) per [Testing Strategy](../gitbooks/developing/testing-strategy.md#failure-path-requirement) - [x] **Diff coverage ≥ 80%** — changed lines (Vitest + cargo-llvm-cov merged via `diff-cover`) meet the gate enforced by [`.github/workflows/coverage.yml`](../.github/workflows/coverage.yml). CI coverage gate must confirm this; local focused Rust tests cover the changed paths. - [x] Coverage matrix updated — N/A: security boundary fix covered by focused Rust tests; no feature matrix row added/removed/renamed. - [x] All affected feature IDs from the matrix are listed in the PR description under `## Related` — N/A: no coverage-matrix feature row applies. - [x] No new external network dependencies introduced (mock backend used per [Testing Strategy](../gitbooks/developing/testing-strategy.md#mock-policy)) - [x] Manual smoke checklist updated if this touches release-cut surfaces — N/A: Core RPC header behavior only; no manual release checklist surface changed. - [x] Linked issue closed via `Closes #NNN` in the `## Related` section ## Impact - Security: arbitrary non-allowlisted browser origins no longer receive ACAO for local Core RPC responses. - Compatibility: Tauri webview origins, loopback dev/E2E origins, and non-browser callers remain supported. - Operators can still add exact additional debug origins with `OPENHUMAN_CORE_ALLOWED_ORIGINS`. ## Related - Closes #2262 - Supersedes #2266 because I do not have permission to push review fixes to `leighstillard/fix/cors-allowlist`. - Follow-up PR(s)/TODOs: none. --- ## AI Authored PR Metadata (required for Codex/Linear PRs) ### Linear Issue - Key: N/A - URL: N/A ### Commit & Branch - Branch: `fix/2262-cors-allowlist` - Commit SHA: `9d1341cff29ef1e1b08721885124ce3267f4a99d` ### Validation Run - [x] `pnpm --filter openhuman-app format:check` — N/A: Rust-only Core RPC change. - [x] `pnpm typecheck` — N/A: Rust-only Core RPC change. - [x] Focused tests: `GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml cors_tests --lib` — 8 passed. - [x] Rust fmt/check (if changed): `cargo fmt --manifest-path Cargo.toml --all`; `GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml --lib`; `git diff --check`. - [x] Tauri fmt/check (if changed): N/A: Tauri shell unchanged. ### Validation Blocked - `command:` N/A - `error:` N/A - `impact:` N/A ### Behavior Changes - Intended behavior change: Core RPC only echoes `Access-Control-Allow-Origin` for allowlisted browser origins instead of wildcard `*`. - User-visible effect: none expected for packaged app, loopback dev, E2E, or non-browser callers. ### Parity Contract - Legacy behavior preserved: Tauri origins, loopback origins, debug env overrides, CORS methods/headers/max-age, and no-Origin non-browser callers remain supported. - Guard/fallback/dispatch parity checks: focused CORS unit tests cover allowed origins, denied origins, no-Origin callers, exact env override matching, and preserved `Vary` values. ### Duplicate / Superseded PR Handling - Duplicate PR(s): #2266 - Canonical PR: this PR if maintainers prefer an immediately updated branch; otherwise #2266 can cherry-pick `9d1341cff29ef1e1b08721885124ce3267f4a99d`. - Resolution (closed/superseded/updated): #2266 remains open; this PR carries the requested review fixes because direct push to the fork branch was denied. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Tightened CORS handling to enforce an origin allowlist; only trusted local schemes and loopback addresses are allowed by default, and disallowed origins no longer receive CORS responses. * **Chores** * Added support for configuring extra allowed origins via environment configuration. * **Tests** * Added comprehensive tests for allowlist decisions, header emission (including Vary behavior), and edge cases. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2328?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Leigh Stillard <leigh@stillard.com> Co-authored-by: 李冠辰 <liguanchen@xiaomi.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
## Summary - Replaces wildcard Core RPC CORS behavior with an explicit allowlist for Tauri and loopback origins. - Preserves existing `Vary` response values while appending `Origin`. - Removes unsafe process-global env mutation from CORS tests by injecting env override input directly. - Adds regression coverage for env override exact matching and existing `Vary` preservation. ## Problem - `src/core/jsonrpc.rs` emitted `Access-Control-Allow-Origin: *`, so any browser origin that obtained the bearer token could call the local RPC surface. - The prior fix in tinyhumansai#2266 addressed the core issue but still had two review blockers: unsafe env mutation in parallel Rust tests and overwriting existing `Vary` headers. - I could not push directly to tinyhumansai#2266's fork branch, so this PR carries the same security fix plus the review follow-ups. ## Solution - Keep `is_origin_allowed(origin)` as the production env-reading entry point. - Add `is_origin_allowed_with_extra(origin, extra_origins)` so tests can exercise override parsing without mutating process-global environment. - Change `with_cors_headers` from `headers.insert(Vary, Origin)` to `headers.append(Vary, Origin)`. - Add focused tests for existing `Vary` preservation and exact-match override behavior. ## Submission Checklist - [x] Tests added or updated (happy path + at least one failure / edge case) per [Testing Strategy](../gitbooks/developing/testing-strategy.md#failure-path-requirement) - [x] **Diff coverage ≥ 80%** — changed lines (Vitest + cargo-llvm-cov merged via `diff-cover`) meet the gate enforced by [`.github/workflows/coverage.yml`](../.github/workflows/coverage.yml). CI coverage gate must confirm this; local focused Rust tests cover the changed paths. - [x] Coverage matrix updated — N/A: security boundary fix covered by focused Rust tests; no feature matrix row added/removed/renamed. - [x] All affected feature IDs from the matrix are listed in the PR description under `## Related` — N/A: no coverage-matrix feature row applies. - [x] No new external network dependencies introduced (mock backend used per [Testing Strategy](../gitbooks/developing/testing-strategy.md#mock-policy)) - [x] Manual smoke checklist updated if this touches release-cut surfaces — N/A: Core RPC header behavior only; no manual release checklist surface changed. - [x] Linked issue closed via `Closes #NNN` in the `## Related` section ## Impact - Security: arbitrary non-allowlisted browser origins no longer receive ACAO for local Core RPC responses. - Compatibility: Tauri webview origins, loopback dev/E2E origins, and non-browser callers remain supported. - Operators can still add exact additional debug origins with `OPENHUMAN_CORE_ALLOWED_ORIGINS`. ## Related - Closes tinyhumansai#2262 - Supersedes tinyhumansai#2266 because I do not have permission to push review fixes to `leighstillard/fix/cors-allowlist`. - Follow-up PR(s)/TODOs: none. --- ## AI Authored PR Metadata (required for Codex/Linear PRs) ### Linear Issue - Key: N/A - URL: N/A ### Commit & Branch - Branch: `fix/2262-cors-allowlist` - Commit SHA: `9d1341cff29ef1e1b08721885124ce3267f4a99d` ### Validation Run - [x] `pnpm --filter openhuman-app format:check` — N/A: Rust-only Core RPC change. - [x] `pnpm typecheck` — N/A: Rust-only Core RPC change. - [x] Focused tests: `GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml cors_tests --lib` — 8 passed. - [x] Rust fmt/check (if changed): `cargo fmt --manifest-path Cargo.toml --all`; `GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml --lib`; `git diff --check`. - [x] Tauri fmt/check (if changed): N/A: Tauri shell unchanged. ### Validation Blocked - `command:` N/A - `error:` N/A - `impact:` N/A ### Behavior Changes - Intended behavior change: Core RPC only echoes `Access-Control-Allow-Origin` for allowlisted browser origins instead of wildcard `*`. - User-visible effect: none expected for packaged app, loopback dev, E2E, or non-browser callers. ### Parity Contract - Legacy behavior preserved: Tauri origins, loopback origins, debug env overrides, CORS methods/headers/max-age, and no-Origin non-browser callers remain supported. - Guard/fallback/dispatch parity checks: focused CORS unit tests cover allowed origins, denied origins, no-Origin callers, exact env override matching, and preserved `Vary` values. ### Duplicate / Superseded PR Handling - Duplicate PR(s): tinyhumansai#2266 - Canonical PR: this PR if maintainers prefer an immediately updated branch; otherwise tinyhumansai#2266 can cherry-pick `9d1341cff29ef1e1b08721885124ce3267f4a99d`. - Resolution (closed/superseded/updated): tinyhumansai#2266 remains open; this PR carries the requested review fixes because direct push to the fork branch was denied. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Tightened CORS handling to enforce an origin allowlist; only trusted local schemes and loopback addresses are allowed by default, and disallowed origins no longer receive CORS responses. * **Chores** * Added support for configuring extra allowed origins via environment configuration. * **Tests** * Added comprehensive tests for allowlist decisions, header emission (including Vary behavior), and edge cases. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2328?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Leigh Stillard <leigh@stillard.com> Co-authored-by: 李冠辰 <liguanchen@xiaomi.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
## Summary - Replaces wildcard Core RPC CORS behavior with an explicit allowlist for Tauri and loopback origins. - Preserves existing `Vary` response values while appending `Origin`. - Removes unsafe process-global env mutation from CORS tests by injecting env override input directly. - Adds regression coverage for env override exact matching and existing `Vary` preservation. ## Problem - `src/core/jsonrpc.rs` emitted `Access-Control-Allow-Origin: *`, so any browser origin that obtained the bearer token could call the local RPC surface. - The prior fix in tinyhumansai#2266 addressed the core issue but still had two review blockers: unsafe env mutation in parallel Rust tests and overwriting existing `Vary` headers. - I could not push directly to tinyhumansai#2266's fork branch, so this PR carries the same security fix plus the review follow-ups. ## Solution - Keep `is_origin_allowed(origin)` as the production env-reading entry point. - Add `is_origin_allowed_with_extra(origin, extra_origins)` so tests can exercise override parsing without mutating process-global environment. - Change `with_cors_headers` from `headers.insert(Vary, Origin)` to `headers.append(Vary, Origin)`. - Add focused tests for existing `Vary` preservation and exact-match override behavior. ## Submission Checklist - [x] Tests added or updated (happy path + at least one failure / edge case) per [Testing Strategy](../gitbooks/developing/testing-strategy.md#failure-path-requirement) - [x] **Diff coverage ≥ 80%** — changed lines (Vitest + cargo-llvm-cov merged via `diff-cover`) meet the gate enforced by [`.github/workflows/coverage.yml`](../.github/workflows/coverage.yml). CI coverage gate must confirm this; local focused Rust tests cover the changed paths. - [x] Coverage matrix updated — N/A: security boundary fix covered by focused Rust tests; no feature matrix row added/removed/renamed. - [x] All affected feature IDs from the matrix are listed in the PR description under `## Related` — N/A: no coverage-matrix feature row applies. - [x] No new external network dependencies introduced (mock backend used per [Testing Strategy](../gitbooks/developing/testing-strategy.md#mock-policy)) - [x] Manual smoke checklist updated if this touches release-cut surfaces — N/A: Core RPC header behavior only; no manual release checklist surface changed. - [x] Linked issue closed via `Closes #NNN` in the `## Related` section ## Impact - Security: arbitrary non-allowlisted browser origins no longer receive ACAO for local Core RPC responses. - Compatibility: Tauri webview origins, loopback dev/E2E origins, and non-browser callers remain supported. - Operators can still add exact additional debug origins with `OPENHUMAN_CORE_ALLOWED_ORIGINS`. ## Related - Closes tinyhumansai#2262 - Supersedes tinyhumansai#2266 because I do not have permission to push review fixes to `leighstillard/fix/cors-allowlist`. - Follow-up PR(s)/TODOs: none. --- ## AI Authored PR Metadata (required for Codex/Linear PRs) ### Linear Issue - Key: N/A - URL: N/A ### Commit & Branch - Branch: `fix/2262-cors-allowlist` - Commit SHA: `9d1341cff29ef1e1b08721885124ce3267f4a99d` ### Validation Run - [x] `pnpm --filter openhuman-app format:check` — N/A: Rust-only Core RPC change. - [x] `pnpm typecheck` — N/A: Rust-only Core RPC change. - [x] Focused tests: `GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml cors_tests --lib` — 8 passed. - [x] Rust fmt/check (if changed): `cargo fmt --manifest-path Cargo.toml --all`; `GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml --lib`; `git diff --check`. - [x] Tauri fmt/check (if changed): N/A: Tauri shell unchanged. ### Validation Blocked - `command:` N/A - `error:` N/A - `impact:` N/A ### Behavior Changes - Intended behavior change: Core RPC only echoes `Access-Control-Allow-Origin` for allowlisted browser origins instead of wildcard `*`. - User-visible effect: none expected for packaged app, loopback dev, E2E, or non-browser callers. ### Parity Contract - Legacy behavior preserved: Tauri origins, loopback origins, debug env overrides, CORS methods/headers/max-age, and no-Origin non-browser callers remain supported. - Guard/fallback/dispatch parity checks: focused CORS unit tests cover allowed origins, denied origins, no-Origin callers, exact env override matching, and preserved `Vary` values. ### Duplicate / Superseded PR Handling - Duplicate PR(s): tinyhumansai#2266 - Canonical PR: this PR if maintainers prefer an immediately updated branch; otherwise tinyhumansai#2266 can cherry-pick `9d1341cff29ef1e1b08721885124ce3267f4a99d`. - Resolution (closed/superseded/updated): tinyhumansai#2266 remains open; this PR carries the requested review fixes because direct push to the fork branch was denied. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Tightened CORS handling to enforce an origin allowlist; only trusted local schemes and loopback addresses are allowed by default, and disallowed origins no longer receive CORS responses. * **Chores** * Added support for configuring extra allowed origins via environment configuration. * **Tests** * Added comprehensive tests for allowlist decisions, header emission (including Vary behavior), and edge cases. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2328?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Leigh Stillard <leigh@stillard.com> Co-authored-by: 李冠辰 <liguanchen@xiaomi.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
## Summary - Replaces wildcard Core RPC CORS behavior with an explicit allowlist for Tauri and loopback origins. - Preserves existing `Vary` response values while appending `Origin`. - Removes unsafe process-global env mutation from CORS tests by injecting env override input directly. - Adds regression coverage for env override exact matching and existing `Vary` preservation. ## Problem - `src/core/jsonrpc.rs` emitted `Access-Control-Allow-Origin: *`, so any browser origin that obtained the bearer token could call the local RPC surface. - The prior fix in tinyhumansai#2266 addressed the core issue but still had two review blockers: unsafe env mutation in parallel Rust tests and overwriting existing `Vary` headers. - I could not push directly to tinyhumansai#2266's fork branch, so this PR carries the same security fix plus the review follow-ups. ## Solution - Keep `is_origin_allowed(origin)` as the production env-reading entry point. - Add `is_origin_allowed_with_extra(origin, extra_origins)` so tests can exercise override parsing without mutating process-global environment. - Change `with_cors_headers` from `headers.insert(Vary, Origin)` to `headers.append(Vary, Origin)`. - Add focused tests for existing `Vary` preservation and exact-match override behavior. ## Submission Checklist - [x] Tests added or updated (happy path + at least one failure / edge case) per [Testing Strategy](../gitbooks/developing/testing-strategy.md#failure-path-requirement) - [x] **Diff coverage ≥ 80%** — changed lines (Vitest + cargo-llvm-cov merged via `diff-cover`) meet the gate enforced by [`.github/workflows/coverage.yml`](../.github/workflows/coverage.yml). CI coverage gate must confirm this; local focused Rust tests cover the changed paths. - [x] Coverage matrix updated — N/A: security boundary fix covered by focused Rust tests; no feature matrix row added/removed/renamed. - [x] All affected feature IDs from the matrix are listed in the PR description under `## Related` — N/A: no coverage-matrix feature row applies. - [x] No new external network dependencies introduced (mock backend used per [Testing Strategy](../gitbooks/developing/testing-strategy.md#mock-policy)) - [x] Manual smoke checklist updated if this touches release-cut surfaces — N/A: Core RPC header behavior only; no manual release checklist surface changed. - [x] Linked issue closed via `Closes #NNN` in the `## Related` section ## Impact - Security: arbitrary non-allowlisted browser origins no longer receive ACAO for local Core RPC responses. - Compatibility: Tauri webview origins, loopback dev/E2E origins, and non-browser callers remain supported. - Operators can still add exact additional debug origins with `OPENHUMAN_CORE_ALLOWED_ORIGINS`. ## Related - Closes tinyhumansai#2262 - Supersedes tinyhumansai#2266 because I do not have permission to push review fixes to `leighstillard/fix/cors-allowlist`. - Follow-up PR(s)/TODOs: none. --- ## AI Authored PR Metadata (required for Codex/Linear PRs) ### Linear Issue - Key: N/A - URL: N/A ### Commit & Branch - Branch: `fix/2262-cors-allowlist` - Commit SHA: `9d1341cff29ef1e1b08721885124ce3267f4a99d` ### Validation Run - [x] `pnpm --filter openhuman-app format:check` — N/A: Rust-only Core RPC change. - [x] `pnpm typecheck` — N/A: Rust-only Core RPC change. - [x] Focused tests: `GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml cors_tests --lib` — 8 passed. - [x] Rust fmt/check (if changed): `cargo fmt --manifest-path Cargo.toml --all`; `GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml --lib`; `git diff --check`. - [x] Tauri fmt/check (if changed): N/A: Tauri shell unchanged. ### Validation Blocked - `command:` N/A - `error:` N/A - `impact:` N/A ### Behavior Changes - Intended behavior change: Core RPC only echoes `Access-Control-Allow-Origin` for allowlisted browser origins instead of wildcard `*`. - User-visible effect: none expected for packaged app, loopback dev, E2E, or non-browser callers. ### Parity Contract - Legacy behavior preserved: Tauri origins, loopback origins, debug env overrides, CORS methods/headers/max-age, and no-Origin non-browser callers remain supported. - Guard/fallback/dispatch parity checks: focused CORS unit tests cover allowed origins, denied origins, no-Origin callers, exact env override matching, and preserved `Vary` values. ### Duplicate / Superseded PR Handling - Duplicate PR(s): tinyhumansai#2266 - Canonical PR: this PR if maintainers prefer an immediately updated branch; otherwise tinyhumansai#2266 can cherry-pick `9d1341cff29ef1e1b08721885124ce3267f4a99d`. - Resolution (closed/superseded/updated): tinyhumansai#2266 remains open; this PR carries the requested review fixes because direct push to the fork branch was denied. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Tightened CORS handling to enforce an origin allowlist; only trusted local schemes and loopback addresses are allowed by default, and disallowed origins no longer receive CORS responses. * **Chores** * Added support for configuring extra allowed origins via environment configuration. * **Tests** * Added comprehensive tests for allowlist decisions, header emission (including Vary behavior), and edge cases. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2328?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Leigh Stillard <leigh@stillard.com> Co-authored-by: 李冠辰 <liguanchen@xiaomi.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
Summary
Replaces
Access-Control-Allow-Origin: *on the in-process JSON-RPC server with an explicit allowlist tied to the origins we actually ship. Possession of the per-launch bearer token is no longer sufficient for an arbitrary browser origin to call/rpc.Closes #2262.
What changes
src/core/jsonrpc.rs:is_origin_allowed(origin)matches against:tauri://localhost,http://tauri.localhost,https://tauri.localhosthttp://127.0.0.1:*,http://localhost:*,http://[::1]:*(Vite dev server, E2E harnesses)OPENHUMAN_CORE_ALLOWED_ORIGINSfor operator-controlled debug harnessescors_middlewarereads the request'sOriginheader and passes it towith_cors_headers.with_cors_headers(response, origin):Access-Control-Allow-Origin.Vary: Originso intermediate caches keep per-origin responses distinct.Originheader) are unaffected.tracing::warn!line on rejected origins for diagnosis.src/core/jsonrpc_cors_tests.rs(new, wired via#[path]likejsonrpc_tests.rs):https://attacker.example, look-alike subdomains,https://localhost,null) refused with no ACAO header.Originheader → no ACAO, butVary: Originstill set.Allow-Methods,Allow-Headers, andMax-Ageare always set.Why
The bearer token is the only protection on
/rpc. Possible token-leak paths include shared error logs, screenshots, Sentry breadcrumbs, or a malicious script inside a CEF child webview (Slack/Gmail/etc.). With wildcard CORS, possession of the token from any origin in any browser is sufficient — the desktop RPC surface includes shell tool calls, memory access, credential reads, and sending messages on the user's behalf, so the blast radius is high.Compatibility
coreRpcClient.tsfetch()path):tauri://localhost/http(s)://tauri.localhostare allowlisted. No frontend change required.pnpm dev(Vite athttp://localhost:1420): allowlisted via the loopback rule.tauri-driveron:4444, Appium): allowlisted via the loopback rule.Origin): unchanged.OPENHUMAN_CORE_ALLOWED_ORIGINS=https://my-harness,....Test plan
cargo test --lib cors_tests— 6 / 6 passing locally.cargo check --lib— clean.cargo fmt --checkon changed files — clean.pnpm dev:appon the Tauri shell and confirm the frontend RPC fetches still succeed.Out of scope
Tracked as follow-ups from the same security audit:
Summary by CodeRabbit
New Features
Security