Skip to content

fix(security): fail closed in pairing.rs::is_authenticated when token is unset (#1919)#2108

Merged
senamakel merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:cursor/a01-1919-pairing-fail-closed
May 20, 2026
Merged

fix(security): fail closed in pairing.rs::is_authenticated when token is unset (#1919)#2108
senamakel merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:cursor/a01-1919-pairing-fail-closed

Conversation

@CodeGhost21
Copy link
Copy Markdown
Contributor

@CodeGhost21 CodeGhost21 commented May 18, 2026

Summary

  • PairingGuard::is_authenticated now fails closed: empty/whitespace bearer tokens are always rejected, and requests are rejected when no paired tokens are configured (regardless of require_pairing).
  • Added ensure_core_rpc_token_for_bind for non-loopback binds: uses a non-empty OPENHUMAN_CORE_TOKEN when set, otherwise auto-generates a 256-bit token and persists it to {workspace}/core.token (same behavior as standalone init_rpc_token in core/auth.rs).
  • New unit tests cover empty-token rejection, valid paired tokens, and public-bind auto-generation / empty-env failure.

Problem

When OPENHUMAN_CORE_HOST is non-loopback (e.g. Docker’s 0.0.0.0) and OPENHUMAN_CORE_TOKEN is unset, pairing could be constructed with require_pairing = false. is_authenticated then returned true for any token—including empty—exposing the full RPC surface on the LAN.

Solution

Auth-check layer (pairing.rs): removed the if !self.require_pairing { return true; } bypass. Authentication always requires a non-empty bearer that matches a configured paired-token hash.

Bind-time token policy: chose (b) auto-generate and persist on public bind without env token (aligned with existing standalone CLI core.token flow). Explicit OPENHUMAN_CORE_TOKEN="" on a public bind returns CoreBindTokenError::EmptyEnvToken.

Coordinates with open PR #2011 (which adds a public-bind warning on src/core/jsonrpc.rs). This PR fixes the actual auth bypass in pairing.rs; #2011’s warning becomes a defense-in-depth complement. Wiring ensure_core_rpc_token_for_bind into server startup can land in a follow-up outside this batch’s owned paths.

Threat model

  • Attack surface: any host on the LAN can reach the RPC port when bound to 0.0.0.0.
  • Capabilities exposed pre-fix: full tool execution, file system access, credential retrieval (effectively RCE).
  • Mitigation: fail-closed auth in is_authenticated + token enforcement at bind time via ensure_core_rpc_token_for_bind.
  • Residual risk after fix: operator sets OPENHUMAN_CORE_TOKEN="" explicitly on a public bind — still rejected with EmptyEnvToken.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/coverage.yml. Run pnpm test:coverage and pnpm test:rust locally; PRs below 80% on changed lines will not merge.
  • Coverage matrix updated — N/A: security-module behavior only; no user-facing feature row change
  • All affected feature IDs from the matrix are listed in the PR description under Related — N/A: no matrix feature IDs
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces (docs/RELEASE-MANUAL-SMOKE.md) — N/A
  • Linked issue closed via Closes #NNN in the Related section

Impact

  • Security: closes unauthenticated RPC when pairing guard is used with require_pairing = false and no tokens.
  • Runtime: Docker/cloud operators without OPENHUMAN_CORE_TOKEN get an auto-generated core.token on public bind (once startup calls ensure_core_rpc_token_for_bind).
  • Compatibility: callers that relied on is_authenticated("") == true with no tokens will now be denied (intended).

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

Commit & Branch

  • Branch: cursor/a01-1919-pairing-fail-closed
  • Commit SHA: b86efe2b2d3733897fa8c29b397fe5ad6daee5ca

Validation Run

  • pnpm --filter openhuman-app format:check — N/A (Rust-only); cargo fmt --all --check passed
  • pnpm typecheck — N/A (no TS changes)
  • Focused tests: cargo test -p openhuman --lib security::pairing — 34/34 passed
  • Rust fmt/check (if changed): cargo fmt --all --check; pre-push pnpm rust:check passed
  • Tauri fmt/check (if changed): N/A

Validation Blocked

  • command: pnpm test:rust (full suite)
  • error: not run in this session (focused pairing tests + pre-push hook rust:check only)
  • impact: full regression signal deferred to CI; pairing module fully covered locally

Behavior Changes

  • Intended behavior change: fail-closed bearer validation; public-bind auto-token generation API
  • User-visible effect: unauthenticated LAN RPC via pairing guard is blocked; operators may see core.token auto-created on public bind once wired

Parity Contract

  • Legacy behavior preserved: loopback bind without env token still returns None from ensure_core_rpc_token_for_bind (local dev)
  • Guard/fallback/dispatch parity checks: is_authenticated no longer short-circuits on require_pairing == false

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this PR
  • Resolution (closed/superseded/updated): N/A

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added RPC token binding functionality with environment variable support.
    • Automatic token generation and secure persistence for public-facing binds.
  • Security Improvements

    • Authentication now fails securely when bearer tokens are empty or unconfigured.
    • Enhanced token validation through secure hashing and set membership verification.
    • Improved error handling for token binding operations.

Review Change Stack

… is unset (tinyhumansai#1919)

Reject empty bearer tokens and unconfigured token sets regardless of
require_pairing. Add ensure_core_rpc_token_for_bind to auto-generate and
persist a core token on non-loopback binds when OPENHUMAN_CORE_TOKEN is unset.

Co-authored-by: Cursor <cursoragent@cursor.com>
@CodeGhost21 CodeGhost21 requested a review from a team May 18, 2026 12:58
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

This PR hardens RPC authentication and introduces token binding for public interfaces to resolve a critical security vulnerability where unauthenticated RPC access was possible when binding on non-loopback addresses without a token. Authentication now fails closed, and public binds automatically generate and persist tokens unless explicitly provided.

Changes

Core RPC Authentication Security

Layer / File(s) Summary
Authentication Hardening
src/openhuman/security/pairing.rs, src/openhuman/security/pairing_tests.rs
PairingGuard::is_authenticated now rejects empty tokens and fails when no paired token hashes are configured, removing the previous short-circuit to true when pairing was not required. Tests verify rejection of empty tokens in both pairing-enabled and pairing-disabled modes.
Core RPC Token Binding Support
src/openhuman/security/pairing.rs, src/openhuman/security/pairing_tests.rs
Adds CORE_TOKEN_ENV_VAR constant, CoreBindTokenError error type, and ensure_core_rpc_token_for_bind function that generates and persists a 256-bit token to core.token with Unix owner-only permissions for non-loopback public binds when no environment token is provided, while returning None for loopback binds and the environment token when explicitly provided. Tests cover auto-generation, persistence, loopback behavior, and environment token handling.
Public API Expansion
src/openhuman/security/mod.rs
Public re-exports are expanded to include ensure_core_rpc_token_for_bind, CoreBindTokenError, and CORE_TOKEN_ENV_VAR.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A token bound to public light,
No longer ghosted in the night,
Authentication fails with care,
Security guards everywhere!
Core binds secure, their purpose clear.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main security fix: failing closed in is_authenticated when bearer token is unset, which directly addresses issue #1919.
Linked Issues check ✅ Passed Changes implement all #1919 objectives: is_authenticated now fails closed for empty tokens with no configured hashes, ensure_core_rpc_token_for_bind provides public-bind auto-generation/empty-env failure policy, and tests validate both behaviors.
Out of Scope Changes check ✅ Passed All changes directly support #1919 objectives: is_authenticated hardening, core token binding logic, and security tests; the mod.rs re-exports expose necessary functions and are in scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/openhuman/security/pairing.rs (1)

310-353: 💤 Low value

Consider logging when empty env token is silently ignored on loopback.

When env_token = Some("") on a loopback bind, the function silently returns Ok(None) without any log. This might make debugging harder if an operator mistakenly sets OPENHUMAN_CORE_TOKEN="" thinking it's configured. Consider adding a debug log similar to line 335-338 to indicate the empty token was ignored.

This is a minor observability improvement—current behavior is safe since loopback doesn't require auth.

🔧 Suggested logging addition
         if is_public_bind(host) {
             log::error!(
                 "[openhuman:pairing] {CORE_TOKEN_ENV_VAR} is set but empty on public bind host={host}"
             );
             return Err(CoreBindTokenError::EmptyEnvToken {
                 host: host.to_string(),
             });
+        } else {
+            log::debug!(
+                "[openhuman:pairing] {CORE_TOKEN_ENV_VAR} is set but empty on loopback host={host}, ignoring"
+            );
         }
     }
🤖 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/openhuman/security/pairing.rs` around lines 310 - 353, In
ensure_core_rpc_token_for_bind, when env_token is Some("") the code currently
logs an error only for public binds and is silent for loopback; add a debug log
in the branch where raw.trim() is empty and is_public_bind(host) is false (the
same branch that later logs loopback info) so operators see that an empty
OPENHUMAN_CORE_TOKEN was intentionally ignored; include host and
CORE_TOKEN_ENV_VAR in the debug message to match existing logging style.
🤖 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.

Nitpick comments:
In `@src/openhuman/security/pairing.rs`:
- Around line 310-353: In ensure_core_rpc_token_for_bind, when env_token is
Some("") the code currently logs an error only for public binds and is silent
for loopback; add a debug log in the branch where raw.trim() is empty and
is_public_bind(host) is false (the same branch that later logs loopback info) so
operators see that an empty OPENHUMAN_CORE_TOKEN was intentionally ignored;
include host and CORE_TOKEN_ENV_VAR in the debug message to match existing
logging style.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d19b7d67-2e00-46ee-9269-055de7268c62

📥 Commits

Reviewing files that changed from the base of the PR and between 70fdedc and b86efe2.

📒 Files selected for processing (3)
  • src/openhuman/security/mod.rs
  • src/openhuman/security/pairing.rs
  • src/openhuman/security/pairing_tests.rs

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid security fix — the fail-closed is_authenticated change correctly eliminates the auth bypass when require_pairing = false. The ensure_core_rpc_token_for_bind API is well-designed with clear error semantics.

File Area Change
pairing.rs Security Removed require_pairing bypass, added empty-token rejection, new bind-time token API
pairing_tests.rs Tests Good coverage of empty tokens, no-tokens-configured, and bind-time auto-generation
mod.rs Exports Re-exports new public symbols

Two minor observations below — neither blocks merge.

}

#[cfg(unix)]
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] write_all(token.as_bytes()) doesn't append a trailing newline. If core/auth.rs::init_rpc_token writes with a newline (common for token files consumed by shell scripts or cat), the two code paths would produce inconsistent files. Worth checking for parity — a \n suffix is cheap insurance.

.create(true)
.truncate(true)
.mode(0o600)
.open(path)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] On Windows the #[cfg(not(unix))] fallback uses std::fs::write which inherits the default ACL — the token file may be readable by other users on the system. If Windows is a supported deployment target for public-bind scenarios, consider using SetNamedSecurityInfo or similar to restrict access. Low priority since Docker/cloud deployments are predominantly Linux.

@senamakel senamakel merged commit e84fc99 into tinyhumansai:main May 20, 2026
27 checks passed
mtkik pushed a commit to mtkik/openhuman-meet that referenced this pull request May 21, 2026
CodeGhost21 added a commit to CodeGhost21/openhuman that referenced this pull request May 22, 2026
AusAgentSmith pushed a commit to AusAgentSmith/openhuman that referenced this pull request May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: Unauthenticated RPC when OPENHUMAN_CORE_HOST=0.0.0.0 without OPENHUMAN_CORE_TOKEN

3 participants