Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Dependencies
node_modules/
vendor/
.pnpm-store/

# Build output
build/
Expand Down
5 changes: 2 additions & 3 deletions includes/class-thirdweb-blocks-support.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,11 @@ public function get_payment_method_data() {
'description' => $this->get_setting('description'),
'supports' => $this->get_supported_features(),

// thirdweb configuration
'clientId' => $this->get_setting('client_id'),
// thirdweb configuration (no Client ID needed for iframe widget)
'seller' => $this->get_setting('seller_wallet'),
'chainId' => (int) $this->get_setting('chain_id'),
'tokenAddress' => $this->get_setting('token_address'),
'theme' => $this->get_setting('theme') ?: 'light',
'theme' => $this->get_setting('theme', 'dark'),

// Icons/branding
'icon' => THIRDWEB_WC_PLUGIN_URL . 'assets/icon.svg',
Expand Down
163 changes: 64 additions & 99 deletions includes/class-thirdweb-payment-gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,11 @@ public function __construct() {
$this->description = $this->get_option('description');
$this->enabled = $this->get_option('enabled');

// thirdweb specific settings
// Use .env value as default if WooCommerce setting is empty
$env_client_id = thirdweb_wc_get_env('THIRDWEB_CLIENT_ID', '');
$this->client_id = $this->get_option('client_id') ?: $env_client_id;
// thirdweb specific settings (no Client ID needed for iframe widget)
$this->seller_wallet = $this->get_option('seller_wallet');
$this->chain_id = $this->get_option('chain_id');
$this->token_address = $this->get_option('token_address');
$this->theme = $this->get_option('theme');
$this->theme = $this->get_option('theme', 'dark');

// Save settings hook
add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);
Expand Down Expand Up @@ -69,23 +66,10 @@ public function init_form_fields() {
'description' => __('Payment method description shown at checkout', 'thirdweb-wc'),
'default' => __('Pay securely with USDC, USDT or other stablecoins from any wallet.', 'thirdweb-wc'),
],
'client_id' => [
'title' => __('thirdweb Client ID', 'thirdweb-wc'),
'type' => 'text',
'description' => sprintf(
__('Get your Client ID from <a href="%s" target="_blank">thirdweb Dashboard</a>. Create a new project if you haven\'t already. Can also be set via .env file for development.', 'thirdweb-wc'),
'https://thirdweb.com/dashboard'
),
'default' => thirdweb_wc_get_env('THIRDWEB_CLIENT_ID', ''),
'placeholder' => __('e.g., abc123def456...', 'thirdweb-wc'),
],
'seller_wallet' => [
'title' => __('Seller Wallet Address', 'thirdweb-wc'),
'type' => 'text',
'description' => sprintf(
__('Your project wallet address that will receive all payments. Get this from your <a href="%s" target="_blank">thirdweb project dashboard</a>.', 'thirdweb-wc'),
'https://thirdweb.com/dashboard'
),
'description' => __('Your wallet address that will receive all payments. Use any Ethereum-compatible wallet (MetaMask, Coinbase Wallet, etc.).', 'thirdweb-wc'),
'default' => '',
'placeholder' => __('0x...', 'thirdweb-wc'),
'custom_attributes' => [
Expand All @@ -108,20 +92,20 @@ public function init_form_fields() {
'token_address' => [
'title' => __('Token Address (Optional)', 'thirdweb-wc'),
'type' => 'text',
'description' => __('USDC/USDT contract address for the chain above. Make sure the token address matches your selected chain. Leave empty to accept the native token (ETH, MATIC, etc.). Default is USDC on Base (chain 8453).', 'thirdweb-wc'),
'description' => __('USDC/USDT contract address for the chain above. Make sure the token address matches your selected chain. Leave empty to accept any stablecoin. Default is USDC on Base (chain 8453).', 'thirdweb-wc'),
'default' => '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
'placeholder' => __('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', 'thirdweb-wc'),
],
'theme' => [
'title' => __('Widget Theme', 'thirdweb-wc'),
'type' => 'select',
'description' => __('Choose the color theme for the checkout widget. Light theme is recommended for most sites.', 'thirdweb-wc'),
'default' => 'light',
'desc_tip' => true,
'description' => __('Choose the theme for the checkout widget. Default is dark.', 'thirdweb-wc'),
'default' => 'dark',
'options' => [
'light' => __('Light', 'thirdweb-wc'),
'dark' => __('Dark', 'thirdweb-wc'),
'light' => __('Light', 'thirdweb-wc'),
],
'desc_tip' => true,
],
];
}
Expand All @@ -134,28 +118,39 @@ public function payment_fields() {
echo wpautop(wptexturize($this->description));
}

// Container for React to mount the CheckoutWidget
echo '<div id="thirdweb-checkout-widget"
data-client-id="' . esc_attr($this->client_id) . '"
data-seller="' . esc_attr($this->seller_wallet) . '"
data-chain-id="' . esc_attr($this->chain_id) . '"
data-token-address="' . esc_attr($this->token_address) . '"
data-amount="' . esc_attr(WC()->cart->get_total('edit')) . '"
data-currency="' . esc_attr(get_woocommerce_currency()) . '">
</div>';
// Container for iframe checkout widget (legacy checkout)
$amount = WC()->cart->get_total('edit');
$params = [
'chain' => $this->chain_id,
'amount' => $amount,
'seller' => $this->seller_wallet,
];

// Only add tokenAddress if provided
if (!empty($this->token_address)) {
$params['tokenAddress'] = $this->token_address;
}

// Add theme (default is dark, but allow override)
$params['theme'] = $this->theme;

// Hidden field to store transaction hash
echo '<input type="hidden" name="thirdweb_tx_hash" id="thirdweb_tx_hash" value="" />';
$iframe_url = 'https://thirdweb.com/bridge/checkout-widget?' . http_build_query($params);

echo '<div id="thirdweb-checkout-widget">';
echo '<iframe src="' . esc_url($iframe_url) . '" height="700px" width="100%" style="border: 0;" title="thirdweb Checkout Widget"></iframe>';
echo '</div>';
}

/**
* Validate payment fields
*
* Note: For WooCommerce Blocks, validation happens in the React component.
* This method is mainly for legacy checkout.
*/
public function validate_fields() {
if (empty($_POST['thirdweb_tx_hash'])) {
wc_add_notice(__('Please complete the payment.', 'thirdweb-wc'), 'error');
return false;
}
// Always return true - validation is handled by:
// 1. React component for Blocks (checks paymentComplete before allowing submission)
// 2. Frontend JavaScript for legacy checkout
return true;
}

Expand All @@ -164,82 +159,52 @@ public function validate_fields() {
*/
public function process_payment($order_id) {
$order = wc_get_order($order_id);
$tx_hash = sanitize_text_field($_POST['thirdweb_tx_hash'] ?? '');

if (empty($tx_hash)) {
// Payment not yet completed - wait for webhook
$order->update_status('pending', __('Awaiting stablecoin payment confirmation.', 'thirdweb-wc'));

return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
}

// Transaction hash provided - verify on-chain
if ($this->verify_transaction($tx_hash, $order)) {
$order->payment_complete($tx_hash);
$order->add_order_note(
sprintf(__('Stablecoin payment completed. Transaction: %s', 'thirdweb-wc'), $tx_hash)
);

WC()->cart->empty_cart();

return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
}

wc_add_notice(__('Payment verification failed. Please try again.', 'thirdweb-wc'), 'error');
return ['result' => 'failure'];
}

/**
* Verify transaction on-chain
*/
private function verify_transaction($tx_hash, $order) {
// Use thirdweb RPC to verify the transaction
$rpc_url = 'https://' . $this->chain_id . '.rpc.thirdweb.com/' . $this->client_id;

$response = wp_remote_post($rpc_url, [
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode([
'jsonrpc' => '2.0',
'method' => 'eth_getTransactionReceipt',
'params' => [$tx_hash],
'id' => 1,
]),
]);

if (is_wp_error($response)) {
return false;
// Get chain ID from payment data (WooCommerce Blocks sends it in payment_data array)
$chain_id = $this->chain_id;

if (isset($_POST['payment_data']) && is_array($_POST['payment_data'])) {
foreach ($_POST['payment_data'] as $data) {
if (isset($data['key']) && $data['key'] === 'thirdweb_chain_id') {
$chain_id = sanitize_text_field($data['value'] ?? $this->chain_id);
break;
}
}
}

$body = json_decode(wp_remote_retrieve_body($response), true);
$receipt = $body['result'] ?? null;

if (!$receipt || $receipt['status'] !== '0x1') {
return false;

// Fallback to legacy format
if ($chain_id === $this->chain_id && isset($_POST['thirdweb_chain_id'])) {
$chain_id = sanitize_text_field($_POST['thirdweb_chain_id']);
}
Comment on lines +166 to 178
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the target file
find . -name "class-thirdweb-payment-gateway.php" -type f

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 131


🏁 Script executed:

# Once we find it, read the full method containing lines 166-178
cat -n includes/class-thirdweb-payment-gateway.php | sed -n '140,200p'

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 2627


🏁 Script executed:

# Search for nonce verification in the file
rg "wp_verify_nonce|check_admin_referer|wp_nonce_field" --type php includes/class-thirdweb-payment-gateway.php

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 68


🏁 Script executed:

# Search for the process_payment method definition to understand the full context
rg -n "function process_payment" --type php -A 30 includes/class-thirdweb-payment-gateway.php

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 1424


🏁 Script executed:

# Check class definition and parent class
rg -n "class.*Payment.*Gateway|extends.*WC_Payment_Gateway" --type php includes/class-thirdweb-payment-gateway.php

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 150


🏁 Script executed:

# Search for any nonce verification anywhere in the file
rg -n "nonce|referer|verify_nonce|check_admin" --type php includes/class-thirdweb-payment-gateway.php

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 68


🏁 Script executed:

# Check if there's any RPC verification or server-side validation
rg -n "rpc|RPC|verify.*transaction|server.*side" --type php includes/class-thirdweb-payment-gateway.php

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 68


🏁 Script executed:

# Search for where process_payment is called/hooked
rg -n "process_payment|add_action.*payment" --type php includes/class-thirdweb-payment-gateway.php

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 255


🏁 Script executed:

# Look at the full file structure to understand initialization and hooks
head -60 includes/class-thirdweb-payment-gateway.php | cat -n

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 2713


🏁 Script executed:

# Check if WooCommerce checkout form itself includes nonce protection
rg -n "woocommerce.*checkout|payment.*form|wp_nonce" --type php | head -50

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 281


🏁 Script executed:

# Look for any frontend files that might handle nonce
find . -name "*.js" -o -name "*.tsx" -o -name "*.jsx" | xargs grep -l "nonce\|referer" 2>/dev/null | head -20

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 68


🏁 Script executed:

# Search for how payment is initiated - check for checkout process
rg -n "checkout|payment.*process|form.*action" --type php includes/ | head -50

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 1934


🏁 Script executed:

# Check the full process_payment implementation and what happens after
cat -n includes/class-thirdweb-payment-gateway.php | sed -n '160,195p'

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 1680


🏁 Script executed:

# Search for any RPC calls or thirdweb API verification anywhere in the codebase
rg -n "rpc\|RPC\|thirdweb.*api\|verify.*transaction\|confirmTransaction" --type php

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 68


🏁 Script executed:

# Check if there's transaction verification happening in any files
find . -type f -name "*.php" | xargs grep -l "confirmTransaction\|verify.*payment\|rpc" 2>/dev/null

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 70


🏁 Script executed:

# Look at the blocks support file to understand how payment is handled there
cat -n includes/class-thirdweb-blocks-support.php | head -100

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 3779


🏁 Script executed:

# Check how WooCommerce Blocks passes data and if there's nonce handling in the JS/React side
find . -name "*.tsx" -o -name "*.ts" -o -name "*.jsx" -o -name "*.js" | xargs grep -l "payment\|nonce\|checkout" 2>/dev/null | head -10

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 181


🏁 Script executed:

# Search the main plugin file for hooks and initialization that might handle nonce
cat -n thirdweb-woocommerce-checkout.php

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 7679


🏁 Script executed:

# Check how WooCommerce Blocks handles checkout nonce verification
web_search "WooCommerce Blocks checkout nonce verification security"

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 136


🏁 Script executed:

# Check the ThirdwebCheckout React component to see if there's any verification happening
head -100 src/checkout-block/ThirdwebCheckout.tsx

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 3297


🌐 Web query:

WooCommerce payment gateway nonce verification security process_payment hook

💡 Result:

Short answer

  • Verify the checkout nonce before you actually process the charge (i.e., before running gateway process_payment). Use the standard WooCommerce checkout nonce name and WordPress functions: check for $_POST['woocommerce-process-checkout-nonce'] (or appropriate Store API/REST nonce for block/REST flows) and call wp_verify_nonce. See WooCommerce/WordPress docs below. [1][2]

Why (brief)

  • process_payment is executed as part of Checkout and typically expects a valid, authenticated request. If you run payment logic without verifying the nonce (and other server-side checks) you increase CSRF/forgery risk and may process unwanted requests. Also ensure the checkout page isn’t cached (cached nonces expire and break verification). [1][3]

Minimal example (classic Checkout page)

  • Put nonce verification early (e.g., in your gateway’s validate_fields() or hooked to woocommerce_checkout_process) and return error if invalid; only call process_payment when the request passed verification.

Example:

process_payment( $order_id ) or continue processing. ?>

Notes / best practices (bullet)

  • For Checkout block / Store API flows use the Store API nonce header ("Nonce") and wp_create_nonce('wc_store_api') / server-side validation per Store API docs. [1]
  • Don’t rely solely on nonce: require SSL, validate/sanitize all inputs, check order/cart state, and rate-limit/block bots (pre-check server-level protections) to reduce card-testing attacks. [1][4][5]
  • Ensure checkout page is excluded from page caching so nonces don’t get stale. [3][6]

References

  • WooCommerce Store API nonces / how to send and validate (Nonce header, wp_create_nonce('wc_store_api')). [1]
  • WooCommerce Payment Gateway API / creating gateways and checkout flow guidance. [2]
  • WooCommerce docs on WordPress nonces and caching implications. [3]
  • WooCommerce security best practices (hardening, validation). [5]
  • Article on preventing card-testing & validating checkout requests (blocking before gateway). [4]

🏁 Script executed:

# Check the React component more to understand the full payment flow
cat -n src/checkout-block/ThirdwebCheckout.tsx | sed -n '1,150p'

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 5812


🏁 Script executed:

# Search for any AJAX handlers or endpoints that might handle payment verification
rg -n "add_action.*ajax\|wp_ajax\|REST" --type php

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 68


🏁 Script executed:

# Check if there's any verification happening after process_payment or in callbacks
cat -n includes/class-thirdweb-payment-gateway.php | tail -50

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 2152


🏁 Script executed:

# Search the entire codebase for RPC calls or blockchain verification
rg -i "rpc\|blockchain\|verify.*transaction\|confirmTransaction\|getTransactionReceipt" --type php --type tsx --type ts

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 113


🏁 Script executed:

# Check the index.tsx to see the complete React component flow
cat -n src/checkout-block/index.tsx

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 3584


🏁 Script executed:

# Check if there's any Store API nonce or REST verification in the plugin
rg -i "store.*api|rest|nonce" --type php includes/

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 68


🏁 Script executed:

# Search for any AJAX or REST endpoints that might intercept the payment
rg -n "rest_api_init|register_rest_route|wp_ajax" --type php

Repository: thirdweb-dev/woocommerce-thirdweb-checkout

Length of output: 68


Add server-side RPC verification before marking orders as complete.

The code accesses $_POST data without explicit nonce verification. More critically, it violates the coding guideline requiring "transaction verification server-side via RPC for security, not client-side." The current implementation marks orders complete immediately upon receiving a postMessage from the thirdweb iframe (line 181), trusting the frontend signal without verifying the actual blockchain transaction via RPC. Implement server-side verification by calling RPC to confirm the transaction occurred on-chain before calling $order->payment_complete().


// Additional verification: check recipient and amount in logs
// This is simplified - production code should decode transfer events
return true;
// Payment was completed via checkout widget - trust thirdweb's confirmation
$order->payment_complete();
$order->add_order_note(
sprintf(
__('Stablecoin payment completed via thirdweb checkout widget. Chain: %s', 'thirdweb-wc'),
$chain_id
)
);

WC()->cart->empty_cart();

return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
}
Comment on lines 160 to 195
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical security concern: Payment completes without any transaction verification.

The process_payment() method calls $order->payment_complete() without verifying that an actual blockchain transaction occurred. This contradicts the coding guideline: "Perform transaction verification server-side via RPC for security, not client-side."

Key risks:

  1. No transaction hash recorded - Orders have no audit trail linking to blockchain transactions
  2. No on-chain verification - The server trusts frontend confirmation without RPC validation
  3. Fraud potential - A malicious actor who can submit the payment form (e.g., via XSS or CSRF) could mark orders as paid without actual payment

The previous implementation had verify_transaction() (even if it had issues noted in past reviews). Removing it entirely significantly degrades security.

Consider at minimum:

  1. Capturing and recording the transaction hash from the widget
  2. Implementing async verification via a webhook or cron job
  3. Setting orders to on-hold status pending manual/automated verification
🔎 Suggested safer approach
     public function process_payment($order_id) {
         $order = wc_get_order($order_id);
         
         // Get chain ID from payment data
         $chain_id = $this->chain_id;
+        $tx_hash = null;
         
         if (isset($_POST['payment_data']) && is_array($_POST['payment_data'])) {
             foreach ($_POST['payment_data'] as $data) {
                 if (isset($data['key']) && $data['key'] === 'thirdweb_chain_id') {
                     $chain_id = sanitize_text_field($data['value'] ?? $this->chain_id);
-                    break;
+                }
+                if (isset($data['key']) && $data['key'] === 'thirdweb_tx_hash') {
+                    $tx_hash = sanitize_text_field($data['value'] ?? '');
                 }
             }
         }

-        // Payment was completed via checkout widget - trust thirdweb's confirmation
-        $order->payment_complete();
+        // Set order on-hold until transaction can be verified
+        $order->update_status('on-hold', 
+            sprintf(
+                __('Awaiting blockchain confirmation. Chain: %s, TX: %s', 'thirdweb-wc'),
+                $chain_id,
+                $tx_hash ?: 'pending'
+            )
+        );
+        
+        // Store TX hash for verification
+        if ($tx_hash) {
+            $order->update_meta_data('_thirdweb_tx_hash', $tx_hash);
+            $order->update_meta_data('_thirdweb_chain_id', $chain_id);
+            $order->save();
+        }

As per coding guidelines, transaction verification should be performed server-side via RPC for security.


/**
* Get config for frontend
*/
public function get_frontend_config() {
return [
'clientId' => $this->client_id,
'seller' => $this->seller_wallet,
'chainId' => (int) $this->chain_id,
'tokenAddress' => $this->token_address,
'theme' => $this->theme,
'title' => $this->title,
'description' => $this->description,
'theme' => $this->theme ?: 'light',
];
}
}
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
"@wordpress/scripts": "^31.1.0",
"typescript": "^5.9.3"
},
"dependencies": {
"thirdweb": "^5.116.1"
},
"dependencies": {},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
Expand Down
Loading