Skip to content

MF3-L12: validate session-key scope ID formats at request boundary#826

Open
philanton wants to merge 1 commit into
fix/audit-findings-finalx3from
fix/mf3-l12
Open

MF3-L12: validate session-key scope ID formats at request boundary#826
philanton wants to merge 1 commit into
fix/audit-findings-finalx3from
fix/mf3-l12

Conversation

@philanton

Copy link
Copy Markdown
Contributor

What

app_sessions.v1.submit_session_key_state only validated array length and lowercasing for application_ids and app_session_ids. It did not validate each ID's format. As a result malformed-but-lowercase IDs passed request validation, reached signature verification, and could persist as inert, unusable scopes (never matching a real application/app-session).

This aligns the handler with the validation already applied in create_app_session and the RPC application_id query-param path.

Changes

  • application_ids: replaced the lowercase-only loop with app.IsValidApplicationID() (regex ^[a-z0-9_-]{1,66}$, which already enforces lowercase).
  • app_session_ids: added core.IsValidHash() (^0x[0-9a-fA-F]{64}$) before the lowercase canonical check. IsValidHash is case-insensitive, so the lowercase check is kept.
  • Both checks run before signature verification — fail fast at the request boundary. Array-length caps unchanged.
  • Tests: refactored the two stale lowercase-only tests into a shared submitStateExpectingError helper; added format-rejection cases (uppercase / illegal char / over-length application IDs; non-hash / short / no-0x / uppercase-hex app-session IDs).

Severity

Low — hardening/consistency, not an exploitable vuln. The request is self-authenticated (both user_sig and session_key_sig gate persistence, so no injection into another user's state), and DB column constraints (VARCHAR(66) / CHAR(66)) already reject oversized values. Worst real case is a user signing their own malformed scope that silently never authorizes anything; this rejects it up front instead.

Test

go build ./nitronode/api/app_session_v1/
go vet  ./nitronode/api/app_session_v1/
go test ./nitronode/api/app_session_v1/ -run TestSubmitSessionKeyState

All green.

🤖 Generated with Claude Code

…dary

submit_session_key_state only checked array length and lowercasing for
ApplicationIDs and AppSessionIDs, letting malformed-but-lowercase IDs
reach signature verification and persist as inert, unusable scopes.

Validate each application_id with app.IsValidApplicationID (subsumes the
lowercase check) and each app_session_id with core.IsValidHash plus the
lowercase canonical check, rejecting both at the request boundary —
matching the validation already applied in create_app_session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 163 files, which is 13 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c843936c-8ef2-49e0-80e2-e24dc2aec6d1

📥 Commits

Reviewing files that changed from the base of the PR and between c0e6677 and 3bbdc1d.

⛔ Files ignored due to path filters (2)
  • sdk/ts-compat/test/unit/__snapshots__/public-api-drift.test.ts.snap is excluded by !**/*.snap
  • sdk/ts/test/unit/__snapshots__/public-api-drift.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (163)
  • cerebro/README.md
  • cerebro/commands.go
  • cerebro/operator.go
  • docs/README.md
  • docs/api.yaml
  • docs/data_models.mmd
  • llms-full.txt
  • nitronode/README.md
  • nitronode/action_gateway/action_gateway.go
  • nitronode/action_gateway/action_gateway_test.go
  • nitronode/action_gateway/interface.go
  • nitronode/action_gateway/permissive_action_allower.go
  • nitronode/api/app_session_v1/README.md
  • nitronode/api/app_session_v1/create_app_session.go
  • nitronode/api/app_session_v1/create_app_session_test.go
  • nitronode/api/app_session_v1/get_app_sessions.go
  • nitronode/api/app_session_v1/get_app_sessions_test.go
  • nitronode/api/app_session_v1/get_last_key_states.go
  • nitronode/api/app_session_v1/handler.go
  • nitronode/api/app_session_v1/interface.go
  • nitronode/api/app_session_v1/rebalance_app_sessions.go
  • nitronode/api/app_session_v1/rebalance_app_sessions_test.go
  • nitronode/api/app_session_v1/submit_app_state.go
  • nitronode/api/app_session_v1/submit_app_state_test.go
  • nitronode/api/app_session_v1/submit_deposit_state.go
  • nitronode/api/app_session_v1/submit_deposit_state_test.go
  • nitronode/api/app_session_v1/submit_session_key_state.go
  • nitronode/api/app_session_v1/submit_session_key_state_test.go
  • nitronode/api/app_session_v1/testing.go
  • nitronode/api/app_session_v1/utils.go
  • nitronode/api/apps_v1/get_apps.go
  • nitronode/api/apps_v1/get_apps_test.go
  • nitronode/api/apps_v1/handler.go
  • nitronode/api/apps_v1/interface.go
  • nitronode/api/apps_v1/submit_app_version.go
  • nitronode/api/apps_v1/submit_app_version_test.go
  • nitronode/api/apps_v1/testing.go
  • nitronode/api/channel_v1/get_channels.go
  • nitronode/api/channel_v1/get_channels_test.go
  • nitronode/api/channel_v1/get_escrow_channel.go
  • nitronode/api/channel_v1/get_escrow_channel_test.go
  • nitronode/api/channel_v1/get_home_channel_test.go
  • nitronode/api/channel_v1/get_last_key_states.go
  • nitronode/api/channel_v1/get_latest_state_test.go
  • nitronode/api/channel_v1/handler.go
  • nitronode/api/channel_v1/interface.go
  • nitronode/api/channel_v1/request_creation.go
  • nitronode/api/channel_v1/request_creation_test.go
  • nitronode/api/channel_v1/submit_session_key_state.go
  • nitronode/api/channel_v1/submit_state.go
  • nitronode/api/channel_v1/submit_state_test.go
  • nitronode/api/channel_v1/testing.go
  • nitronode/api/node_v1/utils.go
  • nitronode/api/rpc_router.go
  • nitronode/api/user_v1/get_action_allowances.go
  • nitronode/api/user_v1/get_action_allowances_test.go
  • nitronode/api/user_v1/get_transactions_test.go
  • nitronode/api/user_v1/handler.go
  • nitronode/api/user_v1/interface.go
  • nitronode/api/user_v1/testing.go
  • nitronode/api/user_v1/utils.go
  • nitronode/chart/README.md
  • nitronode/chart/config/prod-v1/nitronode.yaml.gotmpl
  • nitronode/chart/config/sandbox-v1/nitronode.yaml.gotmpl
  • nitronode/chart/config/stress-v1/nitronode.yaml.gotmpl
  • nitronode/chart/templates/configmap.yaml
  • nitronode/chart/values.yaml
  • nitronode/config/migrations/postgres/20260603000000_drop_app_registry_staking_action_log.sql
  • nitronode/config/schemas/action_gateway_schema.yaml
  • nitronode/event_handlers/service.go
  • nitronode/event_handlers/service_test.go
  • nitronode/event_handlers/testing.go
  • nitronode/main.go
  • nitronode/metrics/exporter.go
  • nitronode/metrics/interface.go
  • nitronode/runtime.go
  • nitronode/store/database/action_log.go
  • nitronode/store/database/action_log_test.go
  • nitronode/store/database/app.go
  • nitronode/store/database/app_session.go
  • nitronode/store/database/app_session_key_state.go
  • nitronode/store/database/app_test.go
  • nitronode/store/database/channel.go
  • nitronode/store/database/channel_session_key_state.go
  • nitronode/store/database/channel_test.go
  • nitronode/store/database/database.go
  • nitronode/store/database/db_store.go
  • nitronode/store/database/db_store_test.go
  • nitronode/store/database/interface.go
  • nitronode/store/database/lifespan_metric_test.go
  • nitronode/store/database/state.go
  • nitronode/store/database/testing.go
  • nitronode/store/database/transaction.go
  • nitronode/store/database/user_staked.go
  • nitronode/store/database/utils.go
  • nitronode/store/database/utils_test.go
  • nitronode/store/memory/asset_config.go
  • nitronode/store/memory/asset_config_test.go
  • nitronode/store/memory/blockchain_config.go
  • nitronode/store/memory/blockchain_config_test.go
  • nitronode/store/memory/memory_store.go
  • pkg/app/app_session_v1.go
  • pkg/app/app_session_v1_test.go
  • pkg/app/app_v1.go
  • pkg/blockchain/evm/app_registry_abi.go
  • pkg/blockchain/evm/channel_hub_reactor.go
  • pkg/blockchain/evm/channel_hub_reactor_test.go
  • pkg/blockchain/evm/init.go
  • pkg/blockchain/evm/locking_client.go
  • pkg/blockchain/evm/locking_reactor.go
  • pkg/blockchain/evm/locking_reactor_test.go
  • pkg/core/event.go
  • pkg/core/interface.go
  • pkg/core/state_advancer_test.go
  • pkg/core/types.go
  • pkg/core/types_test.go
  • pkg/core/utils.go
  • pkg/core/utils_test.go
  • pkg/log/noop_logger.go
  • pkg/rpc/api.go
  • pkg/rpc/client.go
  • pkg/rpc/client_test.go
  • pkg/rpc/connection_hub.go
  • pkg/rpc/dialer.go
  • pkg/rpc/dialer_internal_test.go
  • pkg/rpc/methods.go
  • pkg/rpc/types.go
  • pkg/sign/mock_signer_test.go
  • playground/mockups/history/history-tab.md
  • playground/src/components/HistoryTab.tsx
  • sdk/PROTOCOL_DRIFT_GUARDS.md
  • sdk/go/README.md
  • sdk/go/app_registry.go
  • sdk/go/app_session.go
  • sdk/go/client.go
  • sdk/go/client_test.go
  • sdk/go/examples/app_sessions/lifecycle.go
  • sdk/go/user.go
  • sdk/go/utils.go
  • sdk/mcp/src/index.ts
  • sdk/ts-compat/README.md
  • sdk/ts-compat/src/client.ts
  • sdk/ts-compat/test/unit/client.test.ts
  • sdk/ts/README.md
  • sdk/ts/examples/app_sessions/README.md
  • sdk/ts/examples/app_sessions/lifecycle.ts
  • sdk/ts/examples/example-app/src/components/WalletDashboard.tsx
  • sdk/ts/src/app/packing.ts
  • sdk/ts/src/app/types.ts
  • sdk/ts/src/blockchain/evm/app_registry_abi.ts
  • sdk/ts/src/blockchain/evm/index.ts
  • sdk/ts/src/blockchain/evm/locking_client.ts
  • sdk/ts/src/client.ts
  • sdk/ts/src/core/state.ts
  • sdk/ts/src/core/types.ts
  • sdk/ts/src/rpc/api.ts
  • sdk/ts/src/rpc/client.ts
  • sdk/ts/src/rpc/methods.ts
  • sdk/ts/src/rpc/types.ts
  • sdk/ts/src/utils.ts
  • sdk/ts/test/unit/abi-drift.test.ts
  • sdk/ts/test/unit/core/state_advancer.test.ts
  • sdk/ts/test/unit/rpc-drift.test.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mf3-l12

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.

❤️ Share

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

@philanton philanton changed the base branch from main to fix/audit-findings-finalx3 June 8, 2026 09:13

@nksazonov nksazonov left a comment

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.

Good job — the fix correctly addresses the audit finding by moving both IsValidApplicationID and IsValidHash checks ahead of signature verification, closing the malformed-but-lowercase bypass. Two minor polish items below.

Comment on lines +86 to 92
if !core.IsValidHash(id) {
c.Fail(rpc.Errorf("invalid_session_key_state: app_session_ids must be 0x-prefixed 32-byte hashes, got: %s", id), "")
return
}
if id != strings.ToLower(id) {
c.Fail(rpc.Errorf("invalid_session_key_state: app_session_ids must be lowercase, got: %s", id), "")
return

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.

The two guards produce two different error messages for what the protocol defines as a single rule: "must be a 0x-prefixed 32-byte lowercase hex hash." A caller with an uppercase-hex input (e.g. 0xABCD…) passes the first check and hits the second, receiving a different error code than a caller with a non-hash string. API consumers have to handle two error surfaces for one conceptual constraint.

Suggestion: add core.IsValidLowercaseHash() with regex ^0x[0-9a-f]{64}$ (the exact canonical form), replace both guards with a single call and a single error message, and keep the test coverage for both uppercase-hex and non-hash inputs confirming they both fail via the same path.

if id != strings.ToLower(id) {
c.Fail(rpc.Errorf("invalid_session_key_state: application_ids must be lowercase, got: %s", id), "")
if !app.IsValidApplicationID(id) {
c.Fail(rpc.Errorf("invalid_session_key_state: application_ids must match %s, got: %s", app.ApplicationIDRegex.String(), id), "")

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.

The regex string (^[a-z0-9_-]{1,66}$) in the error message is an internal implementation detail that callers shouldn't need to parse to understand what went wrong. If the regex ever changes, existing clients matching on the error text will silently break.

Suggestion: replace the dynamic app.ApplicationIDRegex.String() with a stable human-readable description, for example: "application_ids must contain only lowercase letters, digits, dashes, and underscores (max 66 chars), got: %s".

@ihsraham ihsraham left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No blockers from me. Approving for L-12 closure.

The fix moves both scope-ID format checks to the request boundary before signature verification and storage. application_ids now use app.IsValidApplicationID, and app_session_ids require a 32-byte hash shape plus lowercase canonical form. The new tests also cover malformed app IDs and app-session IDs and assert those requests do not reach LockSessionKeyState, which is the right proof for this finding.

What is still worth cleaning up is not a closure blocker: the public docs/API wording still describes these arrays as loose lowercase strings rather than the stricter formats now enforced. I would update that in the release/docs pass so clients do not copy the old shape.

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.

3 participants