Skip to main content
WP HealthKit

WooCommerce Product Visibility: Role-Based Access Control

September 20, 202615 min readWooCommerceBy Jamie

Managing WooCommerce product visibility is crucial for any multi-tiered store. Whether you're restricting products to wholesale buyers, hiding sensitive inventory from competitors, or controlling membership-only offerings, implementing proper role-based access control protects your business logic and prevents unauthorized access. WP HealthKit helps identify vulnerabilities in these implementations that could expose restricted products or pricing.

Table of Contents

Understanding WooCommerce Product Visibility Fundamentals

WooCommerce product visibility is the first line of defense against unauthorized viewing. By default, all published products are visible to everyone. However, many stores need granular control—some products for wholesale partners only, others for VIP members, and some for internal use only.

Product visibility in WooCommerce operates at several levels. The basic mechanism uses post status and visibility meta values. When you publish a product, it's assigned a visibility status that determines whether it appears in shop listings, search results, and product archives. However, this default system doesn't inherently understand user roles.

The standard WooCommerce visibility options include public (visible to all), private (not searchable), and hidden (catalog-only). These options work for basic scenarios but fail when you need role-specific hiding. A wholesale customer shouldn't see retail pricing, a guest shouldn't access VIP-only products, and competitors shouldn't find unreleased inventory.

Implementing role-based visibility requires hooking into WooCommerce' core functions. The product query hooks allow you to filter products based on the current user's role before they're retrieved from the database. This approach is more efficient than hiding products in templates—it prevents database queries from returning restricted products.

WP HealthKit scans for improper visibility implementations that might expose restricted products through REST API endpoints, catalog queries, or direct URL access. Without proper validation, a determined user could bypass template-level hiding.

Role-Based Product Access Implementation

Proper role-based access requires a structured approach. The foundation starts with user roles and capabilities. WordPress supports default roles like subscriber, contributor, author, editor, and administrator. WooCommerce adds customer and shop_manager. You'll often create custom roles like wholesale_buyer, vip_customer, or wholesale_distributor.

Each role should have specific product access capabilities. Rather than hiding products through conditional checks, you assign capabilities to roles and use those capabilities to filter queries.

// Add custom capability to wholesale role
$wholesale_role = get_role('wholesale_buyer');
$wholesale_role->add_cap('view_wholesale_products');
$wholesale_role->add_cap('view_wholesale_pricing');

// Restrict product visibility based on capabilities
add_filter('woocommerce_product_is_visible', function($visible, $product_id) {
    $product = wc_get_product($product_id);
    
    if (has_term('wholesale', 'product_cat', $product_id)) {
        // Wholesale category - only visible to wholesale users
        $visible = current_user_can('view_wholesale_products');
    }
    
    if ($product->get_meta('_restricted_role')) {
        $required_role = $product->get_meta('_restricted_role');
        $visible = current_user_can($required_role);
    }
    
    return $visible;
}, 10, 2);

This implementation adds a product-level restriction check. Products can be individually restricted to specific roles via meta values. The meta value approach allows store managers to assign restrictions without code changes.

However, this approach has limitations. It only filters the product visibility in shop displays. REST API endpoints might still expose restricted products. Direct product lookups bypass the visibility filter. Cart operations could potentially add restricted products through direct URLs.

A comprehensive implementation requires filtering at multiple levels: database queries, REST API responses, and direct product access.

// Filter WooCommerce REST API response
add_filter('woocommerce_rest_product_object_query_args', function($args) {
    // Add meta query to exclude restricted products
    if (!current_user_can('manage_options')) {
        $args['meta_query'] = isset($args['meta_query']) ? $args['meta_query'] : [];
        $args['meta_query'][] = [
            'relation' => 'OR',
            [
                'key' => '_restricted_role',
                'compare' => 'NOT EXISTS'
            ],
            [
                'key' => '_restricted_role',
                'value' => wp_get_current_user()->roles,
                'compare' => 'IN'
            ]
        ];
    }
    return $args;
});

// Restrict product access in REST
add_filter('woocommerce_rest_product_object_query_args', function($args) {
    if (!current_user_can('view_product_data')) {
        $args['s'] = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
        // Prevent unauthorized data access
    }
    return $args;
});

This multi-level approach ensures that restricted products can't be accessed through alternative routes. WP HealthKit identifies these gaps by analyzing your implementation across hooks and API endpoints.

Membership Integration Patterns

Membership integration adds another layer to product visibility. Membership plugins like MemberPress or Restrict Content Pro manage user subscriptions and access levels. Your WooCommerce products need to respect these membership statuses.

The pattern involves checking the user's membership status before displaying products. Different membership tiers might have different product catalogs.

// Check membership status for product visibility
add_filter('woocommerce_product_is_visible', function($visible, $product_id) {
    // Get current user's membership level
    $user_id = get_current_user_id();
    
    if (!$user_id) {
        // Not logged in - hide membership products
        $product = wc_get_product($product_id);
        $membership_level = $product->get_meta('_membership_required');
        if ($membership_level) {
            return false;
        }
    }
    
    // Check membership level
    $product = wc_get_product($product_id);
    $required_level = $product->get_meta('_membership_required');
    
    if (!$required_level) {
        return $visible; // No membership requirement
    }
    
    // Use membership plugin function to check status
    if (function_exists('rcp_is_active_member')) {
        if (rcp_is_active_member($user_id, $required_level)) {
            return true;
        }
    }
    
    return false;
}, 10, 2);

This pattern integrates with membership plugins to verify subscription status. However, membership systems have their own security considerations. Expired memberships need to be immediately revoked. Downgraded memberships should restrict product access. Payment failures should disable product visibility.

A common vulnerability occurs when membership status is cached. If a user's membership expires, their visibility permissions might remain cached in sessions or object caches. This requires careful cache invalidation.

// Invalidate visibility cache when membership changes
add_action('rcp_membership_post_activate', function($member) {
    // Clear user's product visibility cache
    wp_cache_delete('user_' . $member->user_id . '_product_visibility');
    
    // Clear REST API cache
    wp_cache_flush_group('woocommerce_products_' . $member->user_id);
});

add_action('rcp_membership_post_deactivate', function($member) {
    wp_cache_delete('user_' . $member->user_id . '_product_visibility');
    wp_cache_flush_group('woocommerce_products_' . $member->user_id);
});

Membership-integrated visibility requires testing across membership state changes. WP HealthKit analyzes your membership integration to ensure proper cache handling and permission checks.

Securing Wholesale Pricing Visibility

Wholesale pricing represents sensitive business data. Retail customers shouldn't see wholesale prices, and wholesale customers shouldn't see retail markup. Exposing pricing information creates competitive intelligence vulnerabilities.

Wholesale visibility involves both product categories and pricing data. Some stores hide entire product categories from retail customers. Others show products but hide pricing until checkout. Still others restrict wholesale products entirely.

// Hide wholesale pricing from retail customers
add_filter('woocommerce_product_get_price_html', function($price, $product) {
    // Check if this is wholesale-only product
    if (has_term('wholesale', 'product_cat', $product->get_id())) {
        if (!current_user_can('view_wholesale_pricing')) {
            return '<span class="wholesale-only">Contact us for pricing</span>';
        }
    }
    
    // Check for wholesale price meta
    $wholesale_only = $product->get_meta('_wholesale_only');
    if ($wholesale_only && !current_user_can('view_wholesale_pricing')) {
        return '<span class="wholesale-only">Wholesale pricing available</span>';
    }
    
    return $price;
}, 10, 2);

// Prevent wholesale pricing in product data
add_filter('woocommerce_product_data_store_cpt_get_products_query', function($query) {
    if (!current_user_can('view_wholesale_pricing')) {
        // Don't return wholesale-only products
        $query['tax_query'][] = [
            'taxonomy' => 'product_cat',
            'field' => 'slug',
            'terms' => 'wholesale',
            'operator' => 'NOT IN'
        ];
    }
    return $query;
});

This approach filters pricing display and product queries based on user capabilities. However, several bypasses are possible. Direct REST API access to product data might expose pricing. Cart item data might contain pricing information. Order history might reveal pricing patterns.

The critical security measure is preventing wholesale pricing leakage through data endpoints.

// Sanitize REST API product data
add_filter('woocommerce_rest_prepare_product_object', function($response, $product, $request) {
    if (!current_user_can('view_wholesale_pricing')) {
        $data = $response->get_data();
        
        // Remove sensitive pricing fields
        unset($data['price']);
        unset($data['regular_price']);
        unset($data['sale_price']);
        unset($data['price_html']);
        
        $response->set_data($data);
    }
    
    return $response;
}, 10, 3);

WP HealthKit identifies wholesale pricing leakage through API inspection and user role simulation across different endpoints.

Need to audit your WooCommerce security configuration? Upload your plugin to WP HealthKit for comprehensive vulnerability scanning.

Preventing Unauthorized Product Discovery

Beyond role-based visibility, preventing unauthorized product discovery requires additional measures. Determined users might bypass role checks through search engine caching, direct URL guessing, or database queries.

Preventing discovery starts with robots.txt and HTTP headers to discourage direct indexing of restricted products.

// Prevent indexing of restricted products
add_action('wp_head', function() {
    $product_id = get_the_ID();
    
    if ($product_id && 'product' === get_post_type($product_id)) {
        $product = wc_get_product($product_id);
        $restricted_role = $product->get_meta('_restricted_role');
        
        if ($restricted_role && !current_user_can($restricted_role)) {
            // Prevent search engine indexing
            echo '<meta name="robots" content="noindex, nofollow">';
            
            // Set 403 status if not authorized
            status_header(403);
        }
    }
});

Direct URL access to restricted products should return 403 Forbidden or redirect to a login page. This prevents URL guessing attacks where users try sequential product IDs.

// Enforce access control on product pages
add_action('wp', function() {
    if (is_product()) {
        $product = wc_get_product(get_the_ID());
        
        if (!$product) {
            wp_die('Product not found', '', ['response' => 404]);
        }
        
        $required_role = $product->get_meta('_restricted_role');
        
        if ($required_role && !current_user_can($required_role)) {
            // Redirect to login or 403
            if (is_user_logged_in()) {
                wp_die('You do not have permission to view this product', '', ['response' => 403]);
            } else {
                wp_safe_remote_post(wp_login_url(get_permalink()));
            }
        }
    }
});

Search results should be filtered at the query level to ensure restricted products never appear. Checking visibility in the template layer leaves products in search results, just hidden visually.

WP HealthKit scans your implementation to ensure product discovery prevention is comprehensive and properly enforced across all access routes.

Testing and Auditing Access Controls

Proper testing ensures your role-based visibility works correctly. Manual testing involves switching user roles and verifying visibility changes. Automated testing should verify visibility across multiple scenarios.

// Test visibility with different roles
function test_product_visibility() {
    $product_id = 123; // Test product
    
    // Test as guest
    wp_set_current_user(0);
    $guest_visible = wc_product_is_visible($product_id);
    
    // Test as retail customer
    $retail_user = new WP_User(get_user_by('login', 'retail_customer'));
    wp_set_current_user($retail_user->ID);
    $retail_visible = wc_product_is_visible($product_id);
    
    // Test as wholesale
    $wholesale_user = new WP_User(get_user_by('login', 'wholesale_customer'));
    wp_set_current_user($wholesale_user->ID);
    $wholesale_visible = wc_product_is_visible($product_id);
    
    // Assertions
    assert(!$guest_visible, "Product should not be visible to guests");
    assert($retail_visible, "Product should be visible to retail");
    assert($wholesale_visible, "Product should be visible to wholesale");
}

Regular audits should check for visibility leakage. WP HealthKit performs continuous scanning to detect when restricted products become exposed.

Common Vulnerabilities and Fixes

Several common vulnerabilities plague role-based product visibility implementations. Understanding these vulnerabilities helps prevent security breaches.

Visibility filtering at wrong hook level: Filtering product visibility too late (in template) leaves products in database queries. Always filter at the query level with pre_get_posts or woocommerce_product_query_meta_query.

Missing REST API filtering: REST API endpoints bypass template filters. Your visibility must be enforced in the REST controller, not just the theme.

Unencrypted product IDs in URLs: If you pass product IDs in plain URLs, users can enumerate products. Use URL parameters sparingly and verify permissions server-side.

Cache poisoning: If visibility is cached per-user, ensure cache is invalidated when roles change. Shared caches might expose restricted products across users.

AJAX endpoint vulnerabilities: Admin-AJAX endpoints often have insufficient permission checks. Always verify user capabilities in AJAX handlers.

// Vulnerable AJAX handler
add_action('wp_ajax_get_product_details', function() {
    $product_id = intval($_POST['product_id']);
    $product = wc_get_product($product_id);
    wp_send_json($product->get_data()); // NO permission check!
});

// Fixed AJAX handler
add_action('wp_ajax_get_product_details', function() {
    check_ajax_referer('product_details_nonce');
    $product_id = intval($_POST['product_id']);
    $product = wc_get_product($product_id);
    
    if (!wc_product_is_visible($product_id)) {
        wp_send_json_error('Product not found');
    }
    
    wp_send_json($product->get_data());
});

Monitoring Access Violations

Monitoring helps detect when unauthorized users attempt to access restricted products. Logging attempts helps identify potential security issues.

// Log access violations
add_action('wp', function() {
    if (is_product()) {
        $product = wc_get_product(get_the_ID());
        $required_role = $product->get_meta('_restricted_role');
        
        if ($required_role && !current_user_can($required_role)) {
            error_log(sprintf(
                'Unauthorized product access attempt: User %d attempted to access product %d',
                get_current_user_id(),
                get_the_ID()
            ));
            
            // Alert admin if repeated attempts
            $user_id = get_current_user_id();
            $attempts = get_transient('product_access_violations_' . $user_id) ?: 0;
            set_transient('product_access_violations_' . $user_id, ++$attempts, HOUR_IN_SECONDS);
            
            if ($attempts > 5) {
                // Send admin notification
                wp_mail(get_option('admin_email'), 'Product Access Violation Alert', 
                    "User $user_id has attempted unauthorized access 5+ times");
            }
        }
    }
});

Proper monitoring enables rapid response to unauthorized access attempts. WP HealthKit includes security scanning to detect unusual access patterns.

Additional Resources

Broader Context and Best Practices

Code quality in WordPress plugins extends far beyond aesthetic preferences or stylistic choices. Quality code is fundamentally about maintainability, which directly impacts security, performance, and reliability over time. When code is well-structured with clear separation of concerns, consistent naming conventions, and comprehensive error handling, bugs are easier to spot, fixes are faster to implement, and new features can be added without introducing regressions.

The WordPress plugin ecosystem benefits enormously from shared coding standards and conventions. When developers follow established patterns for hook usage, option storage, database operations, and API interactions, their code becomes instantly readable to other WordPress developers. This readability matters not just for open-source contributions but also for commercial plugins where team members change over time.

Technical debt in WordPress plugins accumulates silently until it becomes a crisis. Each shortcut taken during development, each deprecated function left in place, each test not written adds to the debt balance. Unlike financial debt, technical debt compounds unpredictably. Proactive quality management through automated code analysis identifies these time bombs before they detonate.

Modern WordPress development demands a level of engineering discipline that matches the platform's maturity. Plugins that started as simple utility scripts a decade ago now handle payment processing, personal data management, and business-critical workflows. Applying professional software engineering practices like automated testing, continuous integration, dependency management, and architectural patterns isn't over-engineering for WordPress.

Broader Industry Context and Best Practices

WooCommerce security extends beyond standard WordPress hardening to address e-commerce-specific attack vectors. Payment processing flows, customer data storage, order manipulation, and inventory management all present unique security challenges. PCI DSS compliance requires specific controls around cardholder data handling, encryption, and access logging. WP HealthKit includes WooCommerce-specific security checks that evaluate payment gateway configurations, customer data protection measures, and order processing integrity. Regular security assessments should test the complete purchase flow, including edge cases like concurrent purchases, payment failures, and refund processing, which often contain subtle vulnerabilities.

WooCommerce performance optimization must account for the additional database complexity that e-commerce introduces. Product catalogs with thousands of items, complex pricing rules, tax calculations, and shipping rate lookups all add processing overhead. Strategic caching must respect dynamic pricing and inventory while still providing acceptable response times. WP HealthKit identifies WooCommerce-specific performance bottlenecks including slow product queries, inefficient cart calculations, and unnecessary order meta queries. Load testing should simulate realistic shopping patterns including browsing, cart management, and checkout flows to identify bottlenecks that only appear under production-like conditions.

Strategic Considerations and Implementation Patterns

WooCommerce checkout optimization balances conversion rate with security and compliance requirements. Streamlined checkout flows reduce abandonment, while strong authentication protects against fraudulent transactions. Guest checkout options, saved payment methods, and express checkout integrations each present different security trade-offs that must be evaluated. WP HealthKit analyzes checkout flow security, identifying potential vulnerabilities in payment processing, session management, and order validation. A/B testing checkout variations provides data-driven insights into which security measures impact conversion rates, enabling informed decisions about the optimal balance between security and user experience.

Frequently Asked Questions

How do I hide products from guests but show them to logged-in users?

Use a visibility filter that checks is_user_logged_in(). Assign guest-hidden products a specific meta value, then filter them based on login status in woocommerce_product_is_visible.

Can I restrict products by membership level without a membership plugin?

Yes, you can create a custom solution using user meta values to track subscription status. Store subscription expiry dates in user meta, then check the expiry in your visibility filter before allowing product access.

What's the best way to handle wholesale pricing securely?

Use role-based capabilities combined with REST API filtering. Hide pricing in templates and data endpoints for users without the view_wholesale_pricing capability. Implement server-side verification on every data request.

How do I prevent users from guessing product IDs to find restricted products?

Return 403 Forbidden status on unauthorized product pages rather than 404, which prevents enumeration. Implement rate limiting on product page requests to prevent brute-force ID enumeration.

Should I hide restricted products or deny access with 403?

A 403 response is more secure than hiding products, as it doesn't require template-level verification. Users immediately understand they lack permission. However, consider your user experience—some stores prefer to show products with "Contact for pricing" messages instead.

How do I audit my current product visibility setup for security issues?

WP HealthKit scans your WooCommerce configuration to identify visibility gaps, missing permission checks, and REST API vulnerabilities. Upload your plugin to receive a detailed security audit with specific recommendations.

Conclusion

Role-based product visibility is essential for protecting sensitive product data and pricing information. A comprehensive implementation requires filtering at multiple levels—database queries, REST API responses, and template rendering. Regular auditing ensures your visibility rules remain enforced as your store evolves.

Don't leave your product visibility to chance. WP HealthKit provides automated security scanning to identify visibility vulnerabilities before they're exploited. Get a comprehensive security audit today—it takes just minutes and provides actionable recommendations for strengthening your product access controls.

Your store's data is only as secure as your least-protected access point. Let WP HealthKit help you secure every endpoint.

Ready to audit your plugin?

WP HealthKit checks for all the issues in this article and 40+ more across 62 verification layers.

Comments