Almost every client we assess uses Cloudflare.
It's the go-to answer when someone asks, "How are you protecting your site?" Cloudflare handles the DDoS mitigation. Cloudflare has a WAF. Cloudflare blocks the bots. And on paper, that's all true.
The problem is the assumption that follows: "I'm behind Cloudflare, so I'm covered."
In our blackbox penetration testing engagements — where we start with only a domain and no credentials, the same starting point as a real attacker — we find critical gaps behind Cloudflare in a significant number of assessments. Not because Cloudflare is broken. Because the configuration around it is.
This post walks through two anonymized case studies based on patterns we've seen repeatedly across real client engagements. Both clients had Cloudflare active. Both had the WAF enabled. Neither had connected the full chain of what it actually takes for Cloudflare to protect them.
A quick principle before we start
Cloudflare only protects traffic that actually flows through Cloudflare.
That sounds obvious. But it has a non-obvious implication: Cloudflare only protects the exact paths that are correctly routed through it, and only for the kinds of problems a proxy or WAF can realistically detect. If your origin server is reachable directly by IP, through an unproxied subdomain, or through a legacy DNS record, Cloudflare does not see that traffic at all. If a development environment is publicly reachable through Cloudflare, Cloudflare may still be in the path — but it does not automatically fix missing authentication, debug mode, or vulnerable application code.
Finding those reachable paths is usually the first thing we do.
Case Study 1 — The Ecommerce Store That "Had Cloudflare"
Background
This client ran a mid-size ecommerce platform. Roughly 10,000 active customers, products across three categories, custom order management built on top of a popular PHP framework. They'd had Cloudflare in place for about eighteen months. The WAF was on its default managed ruleset. They pointed to it as their primary security control.
Our engagement was a full blackbox external penetration test. No credentials, no source code, no VPN access. Just the domain.
Phase 1: Passive recon
Before touching the live site, we spent time on passive reconnaissance.
The first thing we check on any Cloudflare-protected target is whether we can find the real origin IP — the actual server behind the proxy. We started with DNS history.
Using a combination of historical DNS record tools and certificate transparency logs, we pulled a list of IP addresses that the client's domain had pointed to over the past three years. SecurityTrails and crt.sh are the two we reach for first — crt.sh queries certificate transparency logs and often surfaces subdomains or IP addresses that were tied to the domain before a CDN migration.
# Certificate transparency log query
curl -s "https://crt.sh/?q=shop.example.com&output=json" | jq '.[].name_value' | sort -u
# DNS history via SecurityTrails or equivalent
# Returns historical A records with date ranges
One address — an IP on a well-known cloud hosting provider — stood out. It had been the primary A record before they migrated to Cloudflare, and it hadn't been decommissioned.
We then checked Shodan. That same IP was still live. Port 443 open, responding to HTTPS. An Nginx server. The TLS certificate's Common Name matched the client's domain, and the Server header revealed the specific software version — confirming this was the client's origin.
# Shodan CLI query (or search directly at shodan.io)
shodan host <origin-IP>
# Sample output
IP: <origin-IP>
Hostnames: shop.example.com
Ports: 80, 443
443/https: nginx/1.22.1 (Ubuntu)
Certificate CN: shop.example.com
With automated reconnaissance pipelines that query DNS history, cert logs, and Shodan simultaneously, this origin IP discovery now takes under five minutes in most cases. What used to be a manual hour of cross-referencing is now a structured query with near-instant output.
Phase 2: Confirming the bypass
We ran a simple test: a curl request directed at the origin IP, with the correct Host header for the client's domain.
curl -vk --resolve shop.example.com:443:<origin-IP> https://shop.example.com/
The server responded with the full homepage. The response headers told the story clearly:
< HTTP/1.1 200 OK
< Server: nginx/1.22.1 (Ubuntu)
< X-Powered-By: PHP/8.1.2
< Content-Type: text/html; charset=UTF-8
No CF-RAY header. No cf-cache-status. No Server: cloudflare. Those signals are normally present on responses served through Cloudflare; combined with the Nginx server header, their absence confirmed we were talking directly to the origin.
We had bypassed Cloudflare entirely — before attempting a single attack.
Phase 3: The admin panel
With the origin IP confirmed, we started directory discovery against it directly. We used ffuf with a common wordlist, sending requests to the origin IP with the correct Host header — bypassing Cloudflare entirely for this phase.
ffuf -w /usr/share/wordlists/dirb/common.txt \
-u https://<origin-IP>/FUZZ \
-H "Host: shop.example.com" \
-mc 200,301,302,403 \
-o results.json
At this point, the WAF was out of the picture. Cloudflare wasn't intercepting our requests. Whatever protection existed now depended entirely on what the application itself enforced.
Within minutes, ffuf returned a hit at /admin. The login page was accessible. No IP restriction. No additional authentication layer beyond the standard username and password form.
We tested the login form for rate limiting — specifically, whether the application would throttle or block after repeated failed attempts. It did not.
# Simulated rapid sequential requests to the login endpoint
# (scope-limited test — confirmed no lockout, no CAPTCHA, no 429 response)
for i in {1..20}; do
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://<origin-IP>/admin/login \
-H "Host: shop.example.com" \
-d "username=admin&password=test$i"
done
# Output: 200 200 200 200 200 200 200 200 200 200 ...
# No 429, no lockout, no CAPTCHA triggered
Every request returned HTTP 200 with the same "Invalid credentials" page. No lockout. No CAPTCHA. No rate limiting whatsoever.
We didn't pursue credential testing further — that was outside the agreed scope and unnecessary to prove the point. But the implication is clear: an attacker with a credential list and a scripted tool could attempt thousands of login combinations against this panel with zero friction. With no rate limiting and no lockout, this panel was one good wordlist away from being compromised.
Phase 4: IDOR in the order management API
While testing the customer-facing application through the exposed origin, we identified a series of API endpoints used by the order management system.
One endpoint accepted an order ID as a path parameter:
GET /api/orders/{order_id}
We tested whether the application enforced that the authenticated user could only retrieve their own orders. It did not.
We logged in as a test customer with order ID 10452. We then manually incremented the ID:
# Authenticated session cookie captured after login
curl -s -H "Cookie: session=<test-session-token>" \
-H "Host: shop.example.com" \
https://<origin-IP>/api/orders/10451
curl -s -H "Cookie: session=<test-session-token>" \
-H "Host: shop.example.com" \
https://<origin-IP>/api/orders/10440
Both responses returned full order records — belonging to different customers entirely:
{
"order_id": 10451,
"customer_name": "Jane D.",
"email": "[email protected]",
"shipping_address": "14 Grove Street, London, E1 7BX",
"items": [...],
"payment_last4": "3847",
"status": "shipped"
}
No authorization check. No ownership validation. Any authenticated user — including a free account registered with a burner email — could enumerate any order in the system.
This is a textbook Insecure Direct Object Reference (IDOR) vulnerability. By incrementing order_id sequentially, we were able to retrieve full names, shipping addresses, email addresses, order contents, and partial payment information for other customers. An attacker could automate this enumeration across thousands of order IDs in minutes.
This class of vulnerability is also a good illustration of Cloudflare's limits as a sole control: IDOR is a business logic flaw. The requests look structurally identical to legitimate ones. WAFs don't detect them. By reaching the origin directly we were working completely outside any filtering layer, but even through Cloudflare, this finding would have been exploitable.
What this meant for the client
With modern recon tooling, the attacker's path looks like this:
- Origin IP discovery via cert logs + DNS history + Shodan — under 5 minutes
- Confirm direct Cloudflare bypass via
curl— under 2 minutes - Admin panel discovery via
ffufdirectory scan — under 5 minutes - Rate limit test — confirm no lockout or throttling on
/admin/login— 2 minutes - IDOR discovery on order API — under 10 minutes
Total time-to-customer-data: under 25 minutes.
Cloudflare was working as configured. The configuration was the problem.
Case Study 2 — The B2B Web App with an Open Dev Environment
Background
This client ran a B2B web application serving internal workflows for their customers — project tracking, file storage, approval chains. Paying customers, some of them in regulated industries. They had invested meaningfully in their Cloudflare setup: custom WAF ruleset, rate limiting configured, orange cloud on every production DNS record. Their origin server was also locked down correctly — firewall rules restricted inbound traffic on ports 80 and 443 to Cloudflare's published IP ranges only. Traffic could not reach the application server directly without going through Cloudflare first.
On paper, this was a well-architected setup. Cloudflare was genuinely in the path. The origin was not reachable by anyone who wasn't coming through Cloudflare.
Our engagement was a blackbox web application pentest. Same starting conditions: domain only.
Phase 1: Subdomain enumeration
We began with subdomain enumeration using certificate transparency logs, passive discovery sources, active host probing, and public passive DNS data.
# Certificate transparency enumeration
curl -s "https://crt.sh/?q=%.example.com&output=json" | jq '.[].name_value' | sort -u
# Subfinder for passive multi-source subdomain discovery
subfinder -d example.com -silent -o subdomains.txt
# Host discovery with HTTP probing against a targeted wordlist
ffuf -w /usr/share/wordlists/subdomains-top1million.txt \
-u https://FUZZ.example.com \
-mc 200,301,302,403 -t 50
The primary domain — app.example.com — was fully proxied through Cloudflare. Server: cloudflare and CF-RAY were present in response headers. Production was locked down.
But enumeration returned another subdomain: dev.example.com.
Its DNS record resolved to a Cloudflare IP — so it was also behind Cloudflare, orange cloud, traffic routing correctly through the proxy. The origin firewall rules applied here too. Unlike Case Study 1, there was no network-layer bypass opportunity.
But we hit the subdomain anyway.
It loaded. No authentication prompt. No IP restriction. No VPN requirement. A fully functional development version of the same B2B web application — accessible to anyone on the internet who happened to find the subdomain.
Phase 2: Debug mode and verbose error output
The development environment was running with debug mode enabled. Error messages were verbose, exposing stack traces, internal file paths, and database query structure in HTTP responses.
We triggered an error by sending a malformed request to one of the API endpoints:
curl -s "https://dev.example.com/api/v1/accounts?account_id='"
Response:
HTTP/1.1 500 Internal Server Error
Error: SQLSTATE[42000]: Syntax error or access violation near ''' at line 1
Query: SELECT * FROM users WHERE account_id = ''' AND status = 'active'
File: /var/www/dev/api/v1/accounts.php, Line 88
Stack trace:
#0 /var/www/dev/lib/Database.php(112): PDOStatement->execute()
#1 /var/www/dev/api/v1/accounts.php(88): Database->query()
Cloudflare was in the path. The WAF correctly passed the request through to the application, and the application returned a full debug error exposing:
- The technology stack (PHP)
- Exact internal file paths and directory structure
- The raw SQL query being executed, including the parameter name account_id
- Confirmation that the parameter was being passed directly into the query without sanitization
This is exactly the kind of recon that dramatically reduces the effort required for subsequent attacks.
Phase 3: SQL injection through Cloudflare
Armed with the query structure from the debug output, we tested the account_id parameter for SQL injection.
# Time-based blind SQLi test
curl -s "https://dev.example.com/api/v1/accounts?account_id=1' AND SLEEP(5)-- -"
The response was delayed by approximately five seconds. The injection was executing.
We then used a union-based approach to confirm data extraction. Because the original query returned columns mapped to account_id, name, email, and role, we injected matching positional values to pull database metadata into those same fields:
curl -s "https://dev.example.com/api/v1/accounts?account_id=0' UNION SELECT 1,version(),database(),4-- -"
Response:
{
"account_id": "1",
"name": "8.0.32",
"email": "app_dev_db",
"role": "4"
}
The database version appeared in the name field and the database name in the email field — reflecting the column positions in the UNION query. This confirmed exploitable SQL injection in a publicly reachable development environment.
Every request passed through Cloudflare's WAF. The WAF had not blocked it. That does not mean WAFs are useless; it means they are not a substitute for fixing vulnerable application behavior. Managed rules can catch many common SQL injection patterns, but coverage depends on rule sensitivity, payload shape, parameter context, encoding, application behavior, and false-positive tolerance. In this case, the application was accepting attacker-controlled input directly into a database query, and Cloudflare was not the control that should have been relied on to make that safe.
Phase 4: Impact assessment
With SQL injection confirmed in a publicly reachable development environment, the potential impact was serious. We did not need to extract customer data to demonstrate it — the table structure alone was sufficient:
curl -s "https://dev.example.com/api/v1/accounts?account_id=0' UNION SELECT 1,GROUP_CONCAT(table_name),3,4 FROM information_schema.tables WHERE table_schema=database()-- -"
The response listed table names including users, projects, billing, audit_log, and api_tokens.
We stopped here and escalated immediately to the client. The combination of public access, SQL injection, and a database schema that included billing and API tokens made this a critical finding regardless of whether the environment held a production data clone. Development databases are frequently populated from production for realistic testing — meaning that assumption has to be treated as live risk until ruled out.
What this meant for the client
The Cloudflare configuration was solid. The origin was locked down. The WAF was running. None of that mattered, because:
- A development subdomain was publicly accessible with no authentication, no IP restriction, and no VPN requirement
- The dev environment ran the same codebase with debug mode enabled, leaking query structure and file paths
- SQL injection was exploitable in a publicly reachable environment that exposed sensitive database areas —
users,billing,api_tokens
The attacker's path:
- Subdomain enumeration — discover
dev.example.com— under 5 minutes - Access dev environment — no authentication to bypass — immediate
- Trigger verbose error — confirm debug mode, extract query structure — under 2 minutes
- Confirm SQL injection — time-based test on the leaked parameter — under 5 minutes
- Enumerate database structure — list accessible tables and assess impact — under 10 minutes
Total time to confirmed SQL injection in a public dev environment: under 25 minutes.
Cloudflare proxied every single request. It made no difference.
The vulnerability was not in the WAF configuration. It was in the assumption that a dev environment only developers know about is safe to leave open.
What both cases had in common
Neither of these clients was negligent. They had invested in Cloudflare, kept it updated, and thought about security. But in both cases, the serious finding was not in the WAF configuration — it was in the gap between what Cloudflare was configured to protect and what was actually reachable.
In Case Study 1, an exposed origin IP meant Cloudflare was never in the path at all. In Case Study 2, Cloudflare was correctly in the path, but the subdomain it was proxying had no access controls, no secure configuration, and a critical application vulnerability. Different routes to the same outcome: something sensitive was reachable from the public internet when the team assumed they were covered.
Three patterns appear across both:
Incomplete access control on non-production environments. Cloudflare operates per hostname and per route. A strong production setup does not automatically protect every development, staging, or legacy environment. If those environments are public, unauthenticated, and running realistic application code or data structures, they will be found and tested.
A reachable path outside the intended security model. In Case Study 1, that was the origin IP preserved in DNS history. In Case Study 2, it was an unauthenticated subdomain that didn't require any bypass — it was simply open. In both cases, the gap was visible from passive reconnaissance within minutes.
Application-layer vulnerabilities that WAFs don't fix. IDOR and broken access control are logic-level flaws — the requests look structurally legitimate, and WAFs cannot reliably detect them. SQL injection can sometimes be caught by managed WAF rules, but not reliably when the vulnerable parameter, application behavior, or payload shape is specific to the application. The underlying application still needs to be tested and fixed.
What actually helps
These are not all problems a WAF or CDN can solve. Some are configuration checks you can address immediately; others require application security testing.
Origin IP hygiene - Rotate your origin IP after moving to Cloudflare. Old IPs in DNS history are permanent and discoverable in minutes. - Use Cloudflare Tunnel where possible — traffic flows through Cloudflare without any direct server port exposure.
DNS audit - Review every DNS record and confirm orange cloud (proxied) status for anything that serves your application. - gray cloud means directly exposed. That's the correct choice for some records, but make it a deliberate one.
Firewall rules at the origin - Restrict inbound 80/443 to Cloudflare's published IP ranges at the network/firewall level. - Even better, use Authenticated Origin Pulls (mTLS) — Cloudflare presents a certificate your server validates before serving any response.
Lock down staging and development environments - Dev and staging environments should sit behind access controls — IP allowlist, VPN, or HTTP basic auth at minimum. - They often run the same codebase with weaker configuration, and development databases are frequently populated from production for realistic testing. Public access to a dev environment is a serious exposure even before any data is extracted.
Test your application, not just your perimeter - IDOR, SQL injection, broken access control, and other application-layer flaws live in your application logic. A WAF or CDN doesn't fix them. - External automated scans miss most of them. They require a human tester thinking like an attacker.
What a proper external assessment looks at
When we run a blackbox penetration test, we're following roughly the same methodology as a real attacker — because that's the only way to find what a real attacker would find.
That means: - Passive reconnaissance before touching anything live - DNS history and certificate log review - Subdomain enumeration - Origin IP discovery and direct-to-origin validation where a candidate is found - WAF-facing application testing for issues that still matter when Cloudflare is correctly in the path - Application logic testing that goes beyond what automated tools cover
If you haven't had that kind of assessment done, you may have the same gaps these clients did — and not know it.
If you're not ready for a full pentest engagement, our Free Security Snapshot is a starting point. It's a limited external review — we look at your public-facing exposure from the outside, flag obvious gaps, and tell you plainly what an attacker's first twenty minutes would look like against your setup. Submit an inquiry and we'll confirm scope and what's realistic for your setup before anything else.
Not sure what your public-facing security exposure looks like?
Apply for a Free WardenBit Security Snapshot. We review selected websites, web apps, APIs, and ecommerce stores for visible external risk signals and practical next steps - no admin access, passwords, or secrets required.