How I Fixed WooCommerce Cart Contamination Caused by Nginx FastCGI Cache
· 11 min read
A client messaged me on a Friday afternoon — always a Friday — with a screenshot from one of their customers. The customer had added a single dog collar to their cart, but the cart page was showing three items they'd never seen before. Someone else's shopping session, served to the wrong person.
Within an hour, two more customers had reported the same thing. One had reached the checkout page and seen another customer's name and shipping address pre-filled. That's not just a bug. That's a data breach.
The Setup
The site was a UK pet supplies WooCommerce store running on a self-managed VPS: Ubuntu 22.04, Nginx, PHP 8.2-FPM, MariaDB 10.11, and Redis for object caching. I'd set up Nginx FastCGI page caching a few months earlier to handle traffic spikes without scaling the server. The config had been working well — until it wasn't.
Finding the Smoking Gun
My first check was the response headers. I opened the site in an incognito window, added an item to the cart, and inspected the response on the cart page:
curl -sI https://example.com/cart/ | grep -i "x-fastcgi-cache"
X-FastCGI-Cache: HIT
That single word — HIT — told me everything. The cart page was being served from Nginx's cache. A page that should be unique to every visitor was being served as a single cached copy to everyone.
How the Contamination Happened
The sequence was straightforward once I understood it:
- Customer A visits the cart page with 3 items in their session
- Nginx caches that response — including the
Set-Cookieheader containing Customer A's WooCommerce session token - Customer B visits the cart page
- Nginx serves the cached response from step 2
- Customer B's browser now has Customer A's session cookie
- Customer B sees Customer A's cart, name, and address
The cached response wasn't just serving stale HTML. It was serving another user's session cookie, effectively handing over their entire WooCommerce session to the next visitor.
The Broken Configuration
I pulled up the Nginx config for the site. Here's what the cache bypass logic looked like:
set $skip_cache 0;
# Skip cache for logged-in users
if ($http_cookie ~* "wordpress_logged_in_") {
set $skip_cache 1;
}
# Skip cache for POST requests
if ($request_method = POST) {
set $skip_cache 1;
}
# Skip cache for URLs with query strings
if ($query_string != "") {
set $skip_cache 1;
}
The problem was obvious: there were no rules for WooCommerce-specific cookies or pages. The config was a generic WordPress caching setup that had been copied without adapting it for WooCommerce. It skipped cache for logged-in users, but WooCommerce guests — the majority of shoppers — aren't logged in. They rely on session cookies (wp_woocommerce_session_*) that this config completely ignored.
The Fix
I rewrote the bypass rules to handle WooCommerce properly. The fix has three layers.
Layer 1: Exclude WooCommerce Pages by URI
Cart, checkout, and my-account pages must never be cached, regardless of cookies or session state:
# Never cache WooCommerce transactional pages
if ($request_uri ~* "/cart/|/checkout/|/my-account/|/addons/") {
set $skip_cache 1;
}
Layer 2: Bypass Cache on WooCommerce Session Cookies
As soon as a visitor adds an item to their cart, WooCommerce sets cookies. The cache must be bypassed for any request carrying these cookies:
# Skip cache when WooCommerce session cookies are present
if ($http_cookie ~* "wp_woocommerce_session_|woocommerce_items_in_cart|woocommerce_cart_hash") {
set $skip_cache 1;
}
This catches three critical cookies:
wp_woocommerce_session_*— the main session identifierwoocommerce_items_in_cart— set to1when cart has itemswoocommerce_cart_hash— a hash of cart contents
Layer 3: Strip Set-Cookie Headers from Cached Responses
This is the part most guides miss. Even with bypass rules, if a response gets cached before the cookie check triggers, the Set-Cookie header gets baked into the cached response. I added a directive using the ngx_headers_more module to strip Set-Cookie from any response that will be cached:
location ~ \.php$ {
# ... fastcgi params ...
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 60m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# Strip Set-Cookie from responses that will be cached
# Prevents session cookie leakage between visitors
fastcgi_hide_header Set-Cookie;
fastcgi_ignore_headers Set-Cookie;
# Only hide cookies when actually caching (skip_cache = 0)
# Re-add them for bypassed requests
add_header X-FastCGI-Cache $upstream_cache_status;
}
However, fastcgi_hide_header and fastcgi_ignore_headers apply globally to the location block. For more granular control — hiding cookies only on cached responses — use more_clear_headers from the headers-more module:
# In the server block, after the location block
location ~ \.php$ {
# ... existing config ...
# Use headers_more for conditional cookie stripping
set $clear_cookies "";
if ($skip_cache = 0) {
set $clear_cookies "yes";
}
header_filter_by_lua_block {
if ngx.var.clear_cookies == "yes" then
ngx.header["Set-Cookie"] = nil
end
}
}
If you don't have the Lua module, the simpler approach with fastcgi_ignore_headers works — just be aware that WooCommerce pages must be excluded via $skip_cache so their session cookies still function.
Layer 4: Add the Add-to-Cart Query String Bypass
When a customer clicks "Add to Cart" on a product page, WooCommerce uses a query string parameter. This must also bypass the cache.
One gotcha here: nginx's $arg_ prefix only works with underscores, not hyphens. $arg_add-to-cart looks like it should work, but nginx parses it as $arg_add (the value of a query parameter called add) followed by the literal string -to-cart — which is always truthy, effectively disabling your cache on every request. You need a map directive with a regex on $args instead:
# In the http {} block, before your server {} block
map $args $is_add_to_cart {
default 0;
"~(^|&)add-to-cart=" 1;
}
Then in your $skip_cache logic:
# Skip cache for add-to-cart actions
if ($is_add_to_cart) {
set $skip_cache 1;
}
Without this, a customer can click "Add to Cart" and get a cached response where nothing appears to happen.
The Complete Configuration
Here's the full corrected bypass block that I now use as my baseline for any WooCommerce site with Nginx FastCGI caching:
# In the http {} block — map the add-to-cart query parameter
# (nginx $arg_ variables don't support hyphens, so we use a regex map)
map $args $is_add_to_cart {
default 0;
"~(^|&)add-to-cart=" 1;
}
# In the server {} or location block
set $skip_cache 0;
# POST requests
if ($request_method = POST) {
set $skip_cache 1;
}
# URLs with query strings
if ($query_string != "") {
set $skip_cache 1;
}
# WooCommerce transactional pages
if ($request_uri ~* "/cart/|/checkout/|/my-account/|/addons/") {
set $skip_cache 1;
}
# WooCommerce session cookies
if ($http_cookie ~* "wp_woocommerce_session_|woocommerce_items_in_cart|woocommerce_cart_hash") {
set $skip_cache 1;
}
# Logged-in users and commenter cookies
if ($http_cookie ~* "wordpress_logged_in_|comment_author_") {
set $skip_cache 1;
}
# Add-to-cart actions
if ($is_add_to_cart) {
set $skip_cache 1;
}
Testing the Fix
After deploying, I verified every scenario:
# 1. Anonymous visit to a product page — should be cached
curl -sI https://example.com/product/dog-collar/ | grep "X-FastCGI-Cache"
# X-FastCGI-Cache: HIT
# 2. Cart page — should bypass cache
curl -sI https://example.com/cart/ | grep "X-FastCGI-Cache"
# X-FastCGI-Cache: BYPASS
# 3. Request with WooCommerce cookie — should bypass
curl -sI -b "woocommerce_items_in_cart=1" https://example.com/shop/ | grep "X-FastCGI-Cache"
# X-FastCGI-Cache: BYPASS
# 4. Add-to-cart action — should bypass
curl -sI "https://example.com/shop/?add-to-cart=123" | grep "X-FastCGI-Cache"
# X-FastCGI-Cache: BYPASS
All four checks returned the expected cache status. Product pages and the shop index were still being cached for anonymous visitors, keeping the performance benefits. But anything involving a WooCommerce session was bypassed.
What Could Have Gone Worse
This client got lucky. The contamination was spotted within hours because a customer noticed unfamiliar items in their cart. But consider what happens if it goes unnoticed:
- Customer A's shipping address is displayed to Customer B at checkout
- Payment tokens or saved payment method references could leak between sessions
- Order confirmation pages could expose another customer's order details
- Under GDPR, this is a reportable personal data breach — the ICO expects notification within 72 hours
Lessons for Anyone Running Nginx + WooCommerce
-
Never use a generic WordPress Nginx cache config for WooCommerce. WooCommerce's session management is fundamentally different from a standard WordPress blog. The cookies are different, the pages that need to be dynamic are different.
-
Test with
X-FastCGI-Cacheheaders after every config change. Add theadd_header X-FastCGI-Cache $upstream_cache_statusdirective and verify it on every page type — product, cart, checkout, my-account, and shop with a cart cookie set. -
Audit your
Set-Cookiebehaviour. Usecurl -sIto check whether cached responses includeSet-Cookieheaders. If they do, you have a potential session leak. -
Consider Redis page caching instead. Plugins like Powered Cache or custom implementations that use Redis for full-page caching with built-in WooCommerce awareness can avoid this class of bug entirely, because the cache logic lives in PHP where WooCommerce's cookie checks are native.
If you're running WooCommerce on a VPS with Nginx and want someone to audit your caching configuration before something like this happens, take a look at my server management service or WooCommerce maintenance plans. I've seen this exact bug on three separate client sites — it's more common than you'd think.
Stop Firefighting. Start Maintaining.
I manage 70+ WordPress sites for UK agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.
