A private BitTorrent tracker is a web application like any other: authentication, REST API, WebSockets for real-time chat, CDN in front. The difference with a standard SaaS? Members share sensitive data — download ratio, activity history, sometimes their public IP address embedded in .torrent files. A data leak has real consequences.
Methodology — 5 phases
A structured web pentest always follows the same rhythm. In brief:
| Phase | Goal | Tools |
|---|---|---|
| 1. Reconnaissance | Map the attack surface without touching the target | crt.sh, Shodan, Whois, Certificate Transparency |
| 2. Scanning | Identify open ports, services, versions | nmap (carefully, behind Cloudflare) |
| 3. Enumeration | Discover endpoints, parameters, features | Browser DevTools, JS analysis, ffuf |
| 4. Exploitation | Confirm and exploit discovered vulnerabilities | Burp Suite, Python scripts, Playwright |
| 5. Report | Document findings, impact, remediation | Markdown + CVSS scoring |
Phase 1 (recon) is the most important and the most underestimated. The more you understand the target before sending a single request, the more efficient the subsequent phases become.
Recon — stack fingerprinting without touching the server
Certificate Transparency (crt.sh): TLS certificates have been public
by design since 2013. Searching the target domain on
crt.sh
reveals all subdomains that ever had a certificate:
api.trackrx.example, static.trackrx.example,
admin.trackrx.example (not live in production, but worth noting).
Cloudflare fingerprinting: the resolved IPs are Cloudflare anycast IPs.
The real server is masked. Shodan and Censys yield nothing useful about the origin IP.
HTTP headers confirm Cloudflare: CF-Ray, cf-cache-status,
server: cloudflare.
Stack via Nuxt chunks: without even being authenticated,
the homepage source reveals everything.
JavaScript chunks loaded from /_nuxt/ confirm Nuxt.js SSR.
Browsing an entry chunk:
// In /_nuxt/entry.XXXX.js — deobfuscated excerpt
import { defineNuxtPlugin } from '#app'
// Nuxt version exposed in window.__NUXT_DATA__
And in the HTML source, the SSR payload injected by Nuxt:
<script type="application/json" id="__NUXT_DATA__">
[null,"3.15.4","user@example.com","Pseudo123",...]
</script>
The exact Nuxt version, and the logged-in user's email exposed in the HTML. We'll come back to this (VUL-03).
Scanning & Enumeration — closed ports, open endpoints
Limited nmap scan (Cloudflare filters aggressively): only ports 80 and 443 respond on Cloudflare IPs. No direct network attack surface.
However, a .torrent file downloaded from the tracker reveals the internal
hostname in the tracker announce URL:
http://tracker:7070/announce. Confirmed: Docker Compose, the service is
called tracker, port 7070 not publicly exposed.
Ports 3306 (MySQL), 5432 (PostgreSQL), 6379 (Redis) are closed. No direct DB access.
API endpoint discovery: Nuxt chunks contain useFetch()
and $fetch() calls in plain text. Scraping the sources:
# Extract endpoints from JS chunks
curl -s https://trackrx.example/_nuxt/app.XXXX.js | \
grep -oE '"/api/[a-z0-9/_-]+"' | sort -u
Result: thirty or so endpoints documented without even having an account.
/api/torrents, /api/users/:id,
/api/messages/channels/:id/messages,
/api/auth/login, /api/auth/logout, etc.
VUL-01 — CORS misconfiguration (Medium)
CORS (Cross-Origin Resource Sharing) is the mechanism that controls which external websites can make requests to an API from a browser. When a cross-origin request arrives, the server responds with headers that say "I accept requests from these origins."
On TrackrX's authenticated endpoints:
HTTP/2 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
These two headers together — ACAO: * (all origins) +
ACAC: true (send cookies/credentials) — are a contradiction
per the CORS spec.
The myth vs reality: many developers (and scanning tools) flag this combination as a critical vulnerability that allows any malicious site to make authenticated requests to your API. In practice, browsers reject this combination. Chrome, Firefox, Safari: ifACAO: *, credentials are not sent, even ifACAC: true. The spec is explicit about this. The actual risk is low — but the header is incorrect and should be fixed, since a future browser behavior change or a non-browser client could exploit it.
Remediation: explicit origin whitelist on the server side.
// ❌ Before
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// ✅ After
const ALLOWED_ORIGINS = ['https://trackrx.example', 'https://www.trackrx.example'];
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin'); // important for caches
}
VUL-02 — Unbounded chat history (Low)
The messaging system exposes a standard pagination endpoint:
GET /api/messages/channels/1/messages?limit=100&before=MSG_ID
The before parameter allows paginating backwards in time.
With no depth limit, you can scroll back indefinitely. Recursive extraction script:
def fetch_all_history(session, base_url, channel_id):
all_messages = []
oldest_id = None
while True:
params = {"limit": 100}
if oldest_id:
params["before"] = oldest_id
r = session.get(
f"{base_url}/api/messages/channels/{channel_id}/messages",
params=params
)
data = r.json()
if not data:
break
all_messages.extend(data)
oldest_id = min(m["id"] for m in data)
print(f"Total retrieved: {len(all_messages)}")
return all_messages
Result: 60,000+ messages extracted in about 30 minutes with reasonable delays between requests (to avoid triggering Cloudflare rate limiting). The impact isn't a direct compromise — the messages are accessible to logged-in members anyway. But extracting the entire history enables precise member profiling: time patterns, interests, links between pseudonyms and behaviors. A social engineering vector.
Remediation: limit accessible history to 30 days in the query
(WHERE created_at > NOW() - INTERVAL '30 days'),
and/or add specific rate limiting on this endpoint.
VUL-03 — Sensitive data in __NUXT_DATA__ (Low)
Nuxt.js in SSR (Server-Side Rendering) mode generates HTML server-side and sends it
to the client already rendered. To prevent the client-side JavaScript from re-fetching
everything from the API, Nuxt serializes the application's initial state into a
<script type="application/json" id="__NUXT_DATA__"> tag
directly in the HTML.
The issue: this state includes all data loaded server-side at render time — including the logged-in user's email:
<!-- In the profile page HTML -->
<script type="application/json" id="__NUXT_DATA__">
[null,"3.15.4","user@example.com","Pseudo123","2024-01-15T10:23:00Z",...]
</script>
By itself, the logged-in user can see their own email — it's not a third-party leak. The real impact depends on context: shared computer, XSS injection reading this payload, or a DevTools screenshot. Severity stays low, but it's a signal that the SSR data surface deserves attention.
Remediation: systematically audit what gets transmitted to the client
via useAsyncData() and useFetch() on the server side.
Only serialize data strictly necessary for the initial render.
Sensitive user data (email, tokens) can be loaded client-side after hydration,
or explicitly excluded from the SSR payload.
WebSockets behind Cloudflare — the Playwright trick
TrackrX uses Socket.IO for real-time chat. Testing WebSockets during a pentest
is usually straightforward: wscat, a Python script with the
websockets library, or Burp Suite with a WebSocket extension.
Except Cloudflare blocks these clients.
Cloudflare uses the TLS fingerprint (JA3/JA3S) to identify clients, among other signals. A Python script making a TLS connection produces a different signature than a real Chrome browser. Result: 403 or connection refused before even reaching the origin server.
The solution: use a real Chromium driven by Playwright. The browser produces the same TLS fingerprint as a real user. Cloudflare lets it through.
from playwright.async_api import async_playwright
import asyncio
import json
async def capture_ws_frames(url: str, session_cookie: str):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
# Inject the session cookie to be authenticated
await context.add_cookies([{
"name": "__Host-session",
"value": session_cookie,
"domain": "trackrx.example",
"path": "/",
}])
page = await context.new_page()
frames = []
# Intercept incoming WebSocket frames
page.on("websocket", lambda ws: ws.on(
"framereceived",
lambda f: frames.append(f.payload)
))
await page.goto(url)
await asyncio.sleep(5) # give the socket time to receive data
await browser.close()
return frames
def parse_socketio_frame(frame: str) -> dict | None:
"""Parse Socket.IO frames (EIO4 protocol).
Data frames start with '42': '4' = EIO message, '2' = Socket.IO event.
"""
if not frame.startswith("42"):
return None # ping (2), pong (3), connect (40), etc.
try:
data = json.loads(frame[2:])
return {
"event": data[0],
"payload": data[1] if len(data) > 1 else None
}
except (json.JSONDecodeError, IndexError):
return None
async def main():
frames = await capture_ws_frames(
"https://trackrx.example/chat",
session_cookie="YOUR_SESSION_COOKIE"
)
parsed = [parse_socketio_frame(f) for f in frames if isinstance(f, str)]
for event in filter(None, parsed):
print(f"[{event['event']}] {event['payload']}")
asyncio.run(main())
What was observed through this technique: Socket.IO events (message:new,
user:typing, channel:update), payload structures,
and confirmation that data is properly scoped per channel — no observable
cross-channel leak on the client side.
What Cloudflare doesn't do
Cloudflare WAF is excellent at filtering known and volumetric attacks: SQLi, reflected XSS, automated scans, DDoS L3/L4/L7. But certain classes of vulnerabilities pass through by design.
IDOR (Insecure Direct Object Reference): accessing another user's data via their ID. Cloudflare can't distinguish a legitimate request from an IDOR request — both are normal HTTP requests to a known endpoint. Example test:
def test_idor(session, base_url, other_user_id):
"""Test unauthorized access to another user's resources."""
endpoints = [
f"/api/users/{other_user_id}/downloads",
f"/api/users/{other_user_id}/invites",
f"/api/users/{other_user_id}/messages",
]
for ep in endpoints:
r = session.get(f"{base_url}{ep}", timeout=10)
status = "❌ IDOR" if r.status_code == 200 else "✅ OK"
print(f"{r.status_code} {status} {ep}")
On TrackrX, all tested endpoints return 403 for another user's resources. No IDOR. But that's a server-side check — not Cloudflare protecting it.
Business logic: a ratio limit that can be bypassed, an invitation feature that can be abused — Cloudflare doesn't understand your application's business rules. A WAF is a defense layer, not a substitute for correct server-side authorization.
Authenticated requests: once authenticated with a valid account, all requests carry a legitimate session cookie. For Cloudflare, they're indistinguishable from normal traffic. The 60,000+ message extraction (VUL-02) was not blocked.
Positive findings — what's done right
A good pentest isn't just about finding what's wrong. TrackrX has several solid points:
-
Correct security headers: CSP with nonces (no
unsafe-inline), HSTS withmax-age=31536000; includeSubDomains,X-Frame-Options: DENY,X-Content-Type-Options: nosniff. -
__Host-sessioncookie: the__Host-prefix is an underrated but effective protection. A cookie with this prefix is automaticallySecure, without aDomainattribute, and withPath=/— the browser refuses to set it if these conditions aren't met. This prevents certain session fixation attacks via a compromised subdomain. -
Robust CSRF double-submit: CSRF token (
__csrf) sent both as a cookie and as a header, server-generated value, invalidated after use. No obvious bypass. - No observable SQL injection: parameterized ORM, no database errors exposed in responses, generic error messages.
-
Brute force blocked: Cloudflare rate limits login attempts
on
/api/auth/loginafter a few requests. - DB ports closed: 3306, 5432, 6379 not publicly accessible.
-
No exposed admin panels: no accessible
/admin,:8080, or:9090publicly.
Conclusion
On a well-developed application with Cloudflare in front, trivial vulnerabilities (SQLi, reflected XSS, RCE) are rare. What you find is configuration mistakes (CORS), legitimate features with missing constraints (unbounded history), and passive data leaks (NUXT_DATA). Nothing spectacular — but things that deserve fixing.
The signal-to-noise ratio of a pentest on this type of stack is low if you stop at automated scans. The real value is in application understanding: reading JS chunks, understanding auth flows, testing business logic. Cloudflare can't tell the difference between a member browsing their history and a script extracting all of it — that's for the server to catch.
The Playwright trick for WebSockets behind Cloudflare is reusable in any modern web pentest: more and more applications use WebSockets for real-time features, and more and more are behind a WAF that filters classic pentest tooling.