Skip to content

Potential payment verification issue in paid resource gate #11

Description

@chenshj73

Potential payment verification issue in paid resource gate

Hi, I noticed a possible payment-flow issue while reviewing the current repository state. This is a conservative report based on the visible code path, and I may be missing deployment-specific guards outside this repository.

Reviewed HEAD: f0ecbaae6159

What I observed

In the visible payment path, I observed:

One-time x402 proof signs only tx_hash and redemption state is keyed only by tx_hash; resource/path not bound to authorization

Relevant code locations:

  • core/rust/src/middleware/mod.rs
  • core/rust/src/middleware/one_time_payment/verify.rs
  • core/rust/src/middleware/one_time_payment/state.rs
  • core/rust/src/middleware/one_time_payment/utils.rs

Relevant code excerpts

core/rust/src/middleware/mod.rs:18-42

  18 pub use state::MiddlewareState;
  19 #[cfg(not(target_arch = "wasm32"))]
  20 pub use types::{MiddlewareConfig, Scheme, SchemeConfig};
  21 
  22 #[cfg(not(target_arch = "wasm32"))]
  23 use crate::{
  24     error::AuthError,
  25     middleware::{
  26         one_time_payment::{types::OneTimePaymentConfig, verify::verify_tx},
  27         payment_channel::{
  28             types::{PaymentChannel, PaymentChannelConfig},
  29             utils::modify_headers_axum,
  30             verify::verify_and_update_channel,
  31         },
  32         stream_payment::{
  33             types::{StreamsConfig, CFA_V1_FORWARDER_ADDRESS},
  34             verify::verify_stream,
  35         },
  36         types::{PaymentHeader, PaymentPayload},
  37         utils::{
  38             get_current_time, parse_channel_payload, parse_onetime_payload, parse_stream_payload,
  39         },
  40     },
  41 };
  42 

core/rust/src/middleware/one_time_payment/verify.rs:12-36

  12             types::{OneTimePayment, OneTimePaymentConfig, SignedPaymentTx, ABS_WINDOW_SEC},
  13             utils::create_tx_message,
  14         },
  15         utils::get_current_time,
  16     },
  17 };
  18 
  19 // For one time payment verification
  20 pub async fn verify_tx(
  21     signed_tx: SignedPaymentTx,
  22     config: OneTimePaymentConfig,
  23 ) -> Result<(OneTimePayment, bool), AuthError> {
  24     // creating the message
  25     let reconstructed_message = create_tx_message(signed_tx.tx_hash);
  26     println!("Message: 0x{}", hex::encode(&reconstructed_message));
  27 
  28     let signature = signed_tx.signature;
  29     println!("Signature: 0x{}", hex::encode(&signature.as_bytes()));
  30 
  31     // recovering the address from the signature
  32     let recovered = match signature.recover_address_from_msg(reconstructed_message) {
  33         Ok(address) => address,
  34         Err(_) => return Err(AuthError::InvalidSignature),
  35     };
  36     println!("Recovered address: {}", recovered);

core/rust/src/middleware/one_time_payment/state.rs:28-52

  28     }
  29 
  30     pub async fn invalidate(&self, tx_hash: FixedBytes<32>) {
  31         let mut payments = self.payments.write().await;
  32         payments.remove(&tx_hash);
  33     }
  34 
  35     // Additional helper methods for one-time payment logic
  36     pub async fn increment_redemptions(&self, tx_hash: FixedBytes<32>) -> Option<u32> {
  37         let mut payments = self.payments.write().await;
  38         if let Some(payment) = payments.get_mut(&tx_hash) {
  39             payment.redemptions += 1;
  40             Some(payment.redemptions)
  41         } else {
  42             None
  43         }
  44     }
  45 
  46     pub async fn set_first_redeemed(&self, tx_hash: FixedBytes<32>, timestamp: u64) -> bool {
  47         let mut payments = self.payments.write().await;
  48         if let Some(payment) = payments.get_mut(&tx_hash) {
  49             if payment.first_reedemed == 0 {
  50                 payment.first_reedemed = timestamp;
  51                 true
  52             } else {

core/rust/src/middleware/one_time_payment/utils.rs:25-49

  25         })
  26         .and_then(|bytes| {
  27             PrimitiveSignature::try_from(bytes.as_slice()).map_err(|_| {
  28                 println!("Failed: Signature conversion");
  29                 AuthError::InvalidSignature
  30             })
  31         })?;
  32 
  33     let tx_hash = headers
  34         .get("X-Transaction")
  35         .ok_or(AuthError::MissingHeaders)?
  36         .to_str()
  37         .map_err(|_| {
  38             AuthError::InvalidHeaders(
  39                 "X-TxHash header contains invalid UTF-8 characters".to_string(),
  40             )
  41         })?;
  42 
  43     let tx_hash = hex::decode(tx_hash).map_err(|_| {
  44         println!("Failed: Message decode");
  45         AuthError::InvalidTransaction("Tx hash decode failed".to_string())
  46     })?;
  47 
  48     let signed_tx = SignedPaymentTx {
  49         signature,

Why this may matter

For agent-facing payment code, untrusted requests that can drive a server-held wallet or signing flow can turn the service itself into the payer or actor.

Suggested check

Consider making the paid-resource path depend on server-trusted payment requirements and a completed payment state. In particular, re-check recipient/payTo, amount, asset, network, nonce/idempotency, resource binding, and settlement result at the exact point where the protected API/tool/content is released.

Conservative caveat

I only reviewed the code visible in this repository at the HEAD above. If deployment-specific middleware or an upstream service enforces the missing binding/settlement invariant, this may already be mitigated there.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions