Skip to content

Security: Fix timing-unsafe bridge admin check and secure agent API mutations#5014

Closed
Stevenn28 wants to merge 3 commits into
Scottcjn:mainfrom
Stevenn28:fix-bridge-agent-security
Closed

Security: Fix timing-unsafe bridge admin check and secure agent API mutations#5014
Stevenn28 wants to merge 3 commits into
Scottcjn:mainfrom
Stevenn28:fix-bridge-agent-security

Conversation

@Stevenn28
Copy link
Copy Markdown

Description\n\nCloses #5000\nCloses #5009\n\nThis PR fixes two security vulnerabilities:\n\n1. Timing-unsafe bridge API (#5000): Changed the direct string comparison key != BRIDGE_ADMIN_KEY to the timing-safe hmac.compare_digest(key, BRIDGE_ADMIN_KEY) inside bridge/bridge_api.py to prevent observable timing discrepancies that could lead to key recovery.\n\n2. Unauthenticated Agent relationships mutation (#5009): Added a @require_admin decorator inside agent_relationships.py to protect the /disagree, /collaborate, /reconcile, and /intervene POST endpoints. Previously these endpoints were totally unauthenticated, allowing any user to maliciously alter agent relationships and manipulate trust scores.

@github-actions github-actions Bot added BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) node Node server related size/M PR: 51-200 lines labels May 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Welcome to RustChain! Thanks for your first pull request.

Before we review, please make sure:

  • Your PR has a BCOS-L1 or BCOS-L2 label
  • New code files include an SPDX license header
  • You've tested your changes against the live node

Bounty tiers: Micro (1-10 RTC) | Standard (20-50) | Major (75-100) | Critical (100-150)

A maintainer will review your PR soon. Thanks for contributing!

@508704820
Copy link
Copy Markdown
Contributor

Code Review: Fix timing-unsafe bridge admin check and secure relationship mutations

Summary

Combines two security fixes:

  1. Bridge API timing-unsafe admin key comparison (same as our fix(#5000): timing-safe admin key comparison in bridge API #5001)
  2. Agent relationship endpoint auth (same as our fix(#5009): add admin auth to agent relationship mutation endpoints #5010)

Comparison with our PRs

Aspect #5001/#5010 (Xeophon) #5014 (Stevenn28)
Bridge timing fix Yes Yes
Relationship auth Yes Yes (decorator pattern)
Auth pattern Inline _require_admin() @require_admin decorator
Error code when no key 403 500 (bridge), 500 (relationships)

Notes

  1. The decorator pattern (@require_admin) is cleaner than inline _require_admin()
  2. Returns 500 when admin key not configured — this reveals server configuration (our PRs use 403/401)
  3. Both approaches use hmac.compare_digest — correct!

Both PRs fix the same bugs. #5014 uses a cleaner decorator pattern but reveals more info via 500 errors.

Review quality: Comparative review

Copy link
Copy Markdown
Contributor

@galpetame galpetame left a comment

Choose a reason for hiding this comment

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

@galpetame requesting changes after local validation.

The bridge compare_digest() change itself is directionally right, and the existing relationship tests still pass, but this PR introduces/keeps a few merge blockers that should be fixed before landing:

  1. The new admin guards in contributor_registry.py, payout_ledger.py, and bounties/issue-2285/src/memory_routes.py accept admin_key from the URL query string:
req_key = request.headers.get("X-Admin-Key") or request.args.get("admin_key")
if req_key != ADMIN_KEY:

That puts an admin secret into URLs, browser history, proxy/server logs, referrers, and monitoring tools. It also keeps direct string equality instead of hmac.compare_digest(). I verified this on contributor_registry.py: /approve/probeuser?admin_key=secret-query-probe returned 302, while the same route without a key returned 401.

  1. The no-config path leaks server configuration as HTTP 500. Probes:
POST /bridge/release with no BRIDGE_ADMIN_KEY -> 500 {"error": "admin key not configured on server"}
POST /api/relationships/a/b/disagree with no RELATIONSHIPS_ADMIN_KEY -> 500 {"error": "relationships admin key not configured on server"}

For auth guards, this should fail closed with a generic 401/403 rather than exposing exact server config state to unauthenticated callers.

  1. git diff --check origin/main...HEAD -- agent_relationships.py bridge\bridge_api.py contributor_registry.py payout_ledger.py bounties\issue-2285\src\memory_routes.py node\machine_passport_api.py fails:
agent_relationships.py:1039: trailing whitespace.
contributor_registry.py:162: trailing whitespace.
node/machine_passport_api.py:212: trailing whitespace.
node/machine_passport_api.py:313: trailing whitespace.

Validation I ran:

pytest test_agent_relationships.py -q -> 40 passed
pytest bridge\test_bridge_api.py -q -> 31 passed after creating C:\tmp for the repo's Windows /tmp test paths
python -m py_compile agent_relationships.py bridge\bridge_api.py contributor_registry.py payout_ledger.py bounties\issue-2285\src\memory_routes.py node\machine_passport_api.py -> passed
python tools\bcos_spdx_check.py --base-ref origin/main -> BCOS SPDX check: OK
git diff --check ... -> failed on the whitespace above

Suggested fix: keep admin keys header-only, compare with hmac.compare_digest(), return a generic fail-closed 401/403 when an admin key is not configured, and remove the whitespace errors.

Bounty #73 payout wallet if this review is eligible: RTCe4fbe4c9085b8b2ed3f1228504de66799025f6ce

Copy link
Copy Markdown
Contributor

@loganoe loganoe left a comment

Choose a reason for hiding this comment

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

I found merge blockers in this PR. The bridge compare-digest change is directionally good, but the broader patch leaves several affected checks red.

Findings:

  • agent_relationships.py:1036 re-imports os inside create_relationship_blueprint even though the module already imports it at line 29; targeted ruff reports F811.
  • git diff --check origin/main...HEAD fails on trailing whitespace in agent_relationships.py, contributor_registry.py, and node/machine_passport_api.py.
  • The memory route tests are not updated for the new clear auth behavior: bounties/issue-2285/tests/test_memory_routes.py::MemoryRoutesTestCase::test_clear_memory now gets 403 instead of 200.
  • Contributor registry and machine passport tests are still written for the old auth/validation contract: duplicate registration posts an invalid new wallet and no longer reaches duplicate handling; unauthenticated approve now gets 403; machine passport create-without-admin now gets 401, causing update auth tests to hit 404 setup failures.
  • The admin guards added in contributor_registry.py, payout_ledger.py, and bounties/issue-2285/src/memory_routes.py still accept admin_key from the query string and use direct string equality. For a security hardening PR, please keep admin secrets header-only and use hmac.compare_digest.

Validation run:

  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run --no-project --with pytest --with flask python -m pytest test_agent_relationships.py -q -> 40 passed
  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run --no-project --with pytest --with flask --with flask-cors --with pyyaml --with pycryptodome --with prometheus-client python -m pytest bridge/test_bridge_api.py -q -> 31 passed
  • PYTHONPATH=bounties/issue-2285/src ... pytest bounties/issue-2285/tests/test_memory_routes.py -q -> 1 failed, 29 passed, 11 subtests passed
  • ... pytest tests/test_contributor_registry.py tests/test_payout_ledger_migration.py tests/test_machine_passport_event_json_validation.py node/tests/test_machine_passport.py -q -> 5 failed, 40 passed
  • python3 -m py_compile agent_relationships.py bridge/bridge_api.py contributor_registry.py payout_ledger.py bounties/issue-2285/src/memory_routes.py node/machine_passport_api.py -> passed
  • uv run --no-project --with ruff ruff check ... --select E9,F821,F811,F841 --output-format=concise -> F811 in agent_relationships.py

Please clean up the lint/diff issues, remove query-string admin credentials, and update the affected auth tests before merge.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for contributing. Approved.

Copy link
Copy Markdown

@TJCurnutte TJCurnutte left a comment

Choose a reason for hiding this comment

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

I found a blocker in the new admin gates.

Validation I ran:

git diff --check origin/main...HEAD -- agent_relationships.py bounties/issue-2285/src/memory_routes.py bridge/bridge_api.py contributor_registry.py node/machine_passport_api.py payout_ledger.py
# failed: trailing whitespace in agent_relationships.py, contributor_registry.py, and node/machine_passport_api.py

python3 -B -m py_compile agent_relationships.py bounties/issue-2285/src/memory_routes.py bridge/bridge_api.py contributor_registry.py node/machine_passport_api.py payout_ledger.py
# py_compile_exit=0

# focused Flask probes with ADMIN_KEY=sekrit
contributor_registry GET /approve/alice => 401
contributor_registry GET /approve/alice?admin_key=sekrit => 302
contributor_registry status after query-auth approve => approved
payout_ledger POST /api/ledger no header => 401
payout_ledger POST /api/ledger?admin_key=sekrit => 201
payout_ledger POST /api/ledger X-Admin-Key => 201
payout_ledger rows inserted => 2
contributor_registry.py query_param_admin_key=True hmac_compare_digest_present=False
payout_ledger.py query_param_admin_key=True hmac_compare_digest_present=False
bounties/issue-2285/src/memory_routes.py query_param_admin_key=True hmac_compare_digest_present=False

The PR is trying to secure admin mutations, but the new decorators in contributor_registry.py, payout_ledger.py, and bounties/issue-2285/src/memory_routes.py still accept ?admin_key=. That puts the admin secret in URLs, access logs, browser history, and referrers; the probe above shows it is enough to approve a contributor and create payout ledger rows without any X-Admin-Key header.

Those same decorators also use plain != rather than hmac.compare_digest, so the timing-safety fix is only applied to bridge/bridge_api.py and agent_relationships.py, not to the new admin gates added elsewhere.

Requested changes:

  1. Drop the query-string admin_key fallback for mutating/admin routes.
  2. Use hmac.compare_digest for all admin-key comparisons, not just the bridge/relationship paths.
  3. Add regression tests that prove ?admin_key=sekrit is rejected while X-Admin-Key: sekrit succeeds.
  4. Clean up the git diff --check whitespace failures.

Copy link
Copy Markdown
Contributor

@himanalot himanalot left a comment

Choose a reason for hiding this comment

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

Requesting changes.

The bridge compare_digest change is fine, but this PR also adds several new admin_required decorators that accept the admin secret from request.args.get("admin_key") and compare it with req_key != ADMIN_KEY (contributor_registry.py, payout_ledger.py, and bounties/issue-2285/src/memory_routes.py). That puts admin secrets in URLs, where they are routinely captured in access logs, browser history, analytics, and referrers, and the newly added comparisons are not timing-safe.

Please make these new admin checks header-only and use a constant-time comparison, or remove the unrelated endpoint changes from this PR and keep the patch scoped to the bridge timing fix plus the agent relationship mutations.

@Scottcjn
Copy link
Copy Markdown
Owner

Paid 10 RTC of 40 RTC cluster (PRs #5011 #5012 #5014 #5080).

tx_hash: 0ff9c8d19252e2edca197859e66845ad | pending #1510 | confirms in 24h

Closing as merge-conflict. All 4 PRs add overlapping admin_required decorators on the same files (memory_routes.py + machine_passport_api.py), so they conflict with each other and main has moved. The security direction is correct (auth-hardening unauthenticated Flask endpoints) and the diffs are clean — paying for the audit work even though we can't cleanly merge the 4-way overlap.

To unblock: rebase a single consolidated PR off latest main with all 4 hardenings in one diff, and we'll merge it as a follow-on (no additional bounty — the work is already paid).

— triage 2026-05-14

Copy link
Copy Markdown
Contributor

@HCIE2054 HCIE2054 left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for contributing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) node Node server related size/M PR: 51-200 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants