MF3-I12, I13, I14: nitronode audit remediations#822
Conversation
Reject channel session-key state submissions whose assets are non-canonical (not lowercased), duplicated, or unsupported by the node, before signature validation and storage. Non-canonical assets are rejected rather than rewritten so the stored value stays identical to the user-signed metadata-hash payload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In SubmitState the default branch silently bootstrapped a synthetic Void state when no stored state existed for an active home channel. An active channel always has its initial state stored atomically by request_creation, so a nil state is an invariant violation, not a first-submit. Fail closed with "active channel has no stored state" instead. The Void fallback stays in the channel-creation path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
StoreUserState only checked the GORM Error from the user_balances Updates() and ignored RowsAffected. A caller that stored a state without first calling LockUserState would silently match zero rows, leaving channel_states and the cached balance divergent without failing the transaction. Require exactly one affected row so the lock-before-store invariant fails closed. Tests stored states without locking, relying on that silent no-op; add a storeLocked helper (lock-then-store, mirroring the runtime) and a negative test asserting the new error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
ihsraham
left a comment
There was a problem hiding this comment.
Thanks Anton. I-13 and I-14 look closed to me, and the new I-12 per-asset checks for canonical, duplicate, and unsupported assets are the right direction. I am requesting changes on the remaining I-12 case because the audit also required empty asset lists to be accepted only as revocation/deactivation states.
| // duplicate, or an asset the node does not support. Assets must already be lowercased because | ||
| // they are persisted lowercased (StoreChannelSessionKeyState) and looked up lowercased | ||
| // (ValidateChannelSessionKeyForAsset); rejecting a non-canonical asset keeps the user-signed | ||
| // payload identical to what is stored. Empty assets lists are allowed; the per-asset checks |
There was a problem hiding this comment.
This leaves one I-12 case open. The audit asked for assets to be empty only when expires_at <= now, so an empty list is an explicit revocation/deactivation state. With this helper taking only assets, a signed state with assets: [] and a future expires_at still passes validation and can become the active current version while authorizing no usable asset.
Before approval, please reject len(coreState.Assets) == 0 when coreState.ExpiresAt is in the future, and add the matching regression test. The duplicate, canonical, and supported-asset checks already look good.
Remediates three audit findings in nitronode. All informational/defense-in-depth — no exploit in the current healthy flow; each hardens an implicit invariant to fail closed.
MF3-I12 — channel session-key asset validation
SubmitStatefor channel session keys only length-checkedcoreState.Assets. NowvalidateSessionKeyAssetsrejects, per asset:asset != strings.ToLower(asset)) — assets are part of the user-signed metadata hash, so the node rejects rather than rewrites, keeping the signed payload identical to what is stored;GetAssetDecimalserrors).Empty asset lists still allowed. Runs before signature validation/storage.
MF3-I13 — reject submit_state on a stateless active channel
SubmitState's default branch silently bootstrapped a syntheticVoidstate when no stored state existed for an active home channel. An active channel always has its initial state stored atomically byrequest_creation, so a nil state is an invariant violation, not a first-submit. Now fails closed withactive channel has no stored state. TheNewVoidStatefallback stays in the channel-creation path.MF3-I14 — assert user_balances update affects one row
StoreUserStatechecked only the GORMErrorfrom theuser_balancesUpdates()and ignoredRowsAffected. A caller that stored a state without first callingLockUserStatewould silently match zero rows, leavingchannel_statesand the cached balance divergent without failing the transaction. Now requires exactly one affected row. Tests that usedStoreUserStatewithout locking are updated to lock-first via astoreLockedhelper, plus a negative test for the new error.All production call sites already lock-before-store / store-initial-state, so no behavior change in the healthy flow.