mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(security): replace wildcard CORS on core RPC
## 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>
This commit is contained in:
co-authored by
Leigh Stillard
Steven Enamakel
parent
48cdd3a2a9
commit
0311d81455
+7
-2
@@ -627,6 +627,11 @@ const ALLOWED_ORIGINS_ENV: &str = "OPENHUMAN_CORE_ALLOWED_ORIGINS";
|
||||
/// loaded in a CEF child webview) must be refused — the bearer token alone
|
||||
/// is not enough authorization without an origin binding.
|
||||
pub(super) fn is_origin_allowed(origin: &str) -> bool {
|
||||
let extra_origins = std::env::var(ALLOWED_ORIGINS_ENV).ok();
|
||||
is_origin_allowed_with_extra(origin, extra_origins.as_deref())
|
||||
}
|
||||
|
||||
pub(super) fn is_origin_allowed_with_extra(origin: &str, extra_origins: Option<&str>) -> bool {
|
||||
// Tauri v2 webview origins. Windows uses an HTTP(S) custom host; macOS
|
||||
// and Linux use the `tauri://` scheme. We accept both for portability.
|
||||
if matches!(
|
||||
@@ -651,7 +656,7 @@ pub(super) fn is_origin_allowed(origin: &str) -> bool {
|
||||
}
|
||||
|
||||
// Env override: comma-separated exact matches.
|
||||
if let Ok(extra) = std::env::var(ALLOWED_ORIGINS_ENV) {
|
||||
if let Some(extra) = extra_origins {
|
||||
for candidate in extra.split(',').map(str::trim).filter(|s| !s.is_empty()) {
|
||||
if candidate == origin {
|
||||
return true;
|
||||
@@ -691,7 +696,7 @@ async fn cors_middleware(req: Request, next: Next) -> Response {
|
||||
/// the calling JS. Non-browser callers (no `Origin` header) are unaffected.
|
||||
pub(super) fn with_cors_headers(mut response: Response, origin: Option<&str>) -> Response {
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(header::VARY, HeaderValue::from_static("Origin"));
|
||||
headers.append(header::VARY, HeaderValue::from_static("Origin"));
|
||||
|
||||
if let Some(o) = origin {
|
||||
if is_origin_allowed(o) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Unit tests for the CORS allowlist and header-emission logic in `jsonrpc.rs`.
|
||||
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::http::{header, HeaderValue, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
|
||||
use super::{is_origin_allowed, with_cors_headers, ALLOWED_ORIGINS_ENV};
|
||||
use super::{is_origin_allowed, is_origin_allowed_with_extra, with_cors_headers};
|
||||
|
||||
fn ok_response() -> Response {
|
||||
(StatusCode::OK, "").into_response()
|
||||
@@ -78,29 +78,46 @@ fn missing_origin_emits_no_acao_but_sets_vary() {
|
||||
|
||||
#[test]
|
||||
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",
|
||||
);
|
||||
}
|
||||
let extra_origins = Some("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"
|
||||
assert!(is_origin_allowed_with_extra(
|
||||
"https://debug.internal",
|
||||
extra_origins
|
||||
));
|
||||
assert!(is_origin_allowed_with_extra(
|
||||
"http://harness:9000",
|
||||
extra_origins
|
||||
));
|
||||
assert!(!is_origin_allowed_with_extra(
|
||||
"https://debug.internal.attacker.example",
|
||||
extra_origins
|
||||
));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
match prev {
|
||||
Some(v) => std::env::set_var(ALLOWED_ORIGINS_ENV, v),
|
||||
None => std::env::remove_var(ALLOWED_ORIGINS_ENV),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn preserves_existing_vary_values() {
|
||||
let mut response = ok_response();
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(header::VARY, HeaderValue::from_static("Accept-Encoding"));
|
||||
|
||||
let r = with_cors_headers(response, None);
|
||||
let values = r
|
||||
.headers()
|
||||
.get_all(header::VARY)
|
||||
.iter()
|
||||
.map(|v| v.to_str().unwrap_or("<invalid>"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(values, vec!["Accept-Encoding", "Origin"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_does_not_allow_lookalike_suffixes() {
|
||||
assert!(!is_origin_allowed_with_extra(
|
||||
"https://debug.internal.attacker.example",
|
||||
Some("https://debug.internal")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user