← Back to Research

Patterns from this week's pentests: the five weakness classes that keep recurring

The recurring shape. No two estates we test look alike, but the findings cluster into the same five classes every week: authentication with no rate-limit or lockout, session cookies missing their flags, TLS hostname and expiry hygiene, IDOR/BOLA on object access, and JWT algorithm confusion. Nothing below names a customer or a domain. These are anonymized, class-level patterns, with a defender checklist for each and how we validate it safely.

We run continuous, evidence-first testing across a portfolio of production environments: different stacks, different frameworks, different decades of accreted decisions. And yet, week after week, the findings cluster into the same handful of classes. This post is the anonymized, class-level read on what recurred this cycle: the shapes of weakness, the checklist that closes each one, and how our continuous testing proves it without ever becoming the attack.

This week the cluster was tight: missing rate-limit and lockout on authentication, session-cookie flag gaps, TLS hostname and expiry hygiene, IDOR/BOLA on object access, and JWT algorithm confusion. Five classes. Every one of them is older than the people exploiting it. Every one of them is still live in production right now.

1. Authentication without a cost: missing rate-limit and lockout

The single most common finding this cycle was a login or token endpoint that imposes no cost on a failed attempt. No lockout after N failures, no per-account throttle, no per-IP backoff, no CAPTCHA escalation, nothing. Credential stuffing against these endpoints is free, and the only thing standing between an attacker and a valid session is the quality of the breach corpus they bought.

The variants we saw:

Defender checklist: enforce throttling at the API tier, not just the UI (test the endpoint the form calls); lock or step up after a small number of failures, per account and per source; normalize response timing and body for “account exists” versus “does not exist”; cap MFA code attempts and expire codes quickly.

We validate this safely by probing with a tiny, bounded burst against a throwaway non-existent account and measuring whether the endpoint imposes any increasing cost. We never run a real credential-stuffing campaign against live accounts. The finding is “the endpoint accepts unbounded attempts,” proven with a handful of requests, not a flood.

2. Session cookies missing their flags

The second class is unglamorous and ubiquitous: session cookies set without HttpOnly, without Secure, without a sane SameSite, or scoped too broadly with Domain. Each missing flag widens the blast radius of an otherwise-contained bug. A reflected XSS that should be a nuisance becomes session theft when the cookie is readable from JavaScript. A SameSite=None cookie without a matching CSRF defense reopens cross-site request forgery.

This class is why the session-token leak family of CVEs hurts so much. CVE-2023-4966 (“CitrixBleed”) and its 2025 successor CVE-2025-5777 (“CitrixBleed 2”) both leak session material straight out of device memory on Citrix NetScaler. When the stolen token is a session cookie with weak flags and a long, unbound lifetime, the leak converts directly into a hijacked, MFA-bypassing session. The CVE is the appliance bug; the amplifier is hygiene.

# Defender's own-surface check: inspect Set-Cookie flags on the session cookie.
curl -sSI "https://app.example/login" \
  | grep -i '^set-cookie:' \
  | while read -r line; do
      echo "$line" | grep -qi 'httponly'  || echo "MISSING HttpOnly:  $line"
      echo "$line" | grep -qi 'secure'    || echo "MISSING Secure:    $line"
      echo "$line" | grep -qi 'samesite'  || echo "MISSING SameSite:  $line"
    done
# A session cookie missing HttpOnly + Secure is a finding on its own merits.

Defender checklist: set HttpOnly, Secure, and SameSite=Lax (or Strict where the flow allows) on every session cookie; scope Domain as narrowly as possible; bind sessions to a transport fingerprint where you can; and age sessions out aggressively so a leaked token has a short useful life.

3. TLS hostname and expiry hygiene

The third recurring class is TLS that is present but wrong. We routinely find certificates that have expired or are within days of expiry on a forgotten subdomain, certificates whose Subject Alternative Names do not actually cover the hostname serving them, wildcard certs stretched across trust boundaries, and endpoints still negotiating deprecated protocol versions or accepting weak cipher suites.

The recurring failure mode is not the primary marketing domain. That one has monitoring. It is the forgotten host: the staging box, the legacy API endpoint, the acquired-company subdomain that nobody re-pointed. This is exactly why deep subdomain enumeration is mandatory pre-work for us: the certificate problems live on the doors nobody is watching.

Defender checklist: inventory every certificate across every discovered subdomain, not just the apex; alert on expiry with weeks of runway, not hours; verify the SAN list actually covers the served hostname; disable protocol versions and cipher suites below your floor; and re-enumerate subdomains continuously so new hosts inherit the same monitoring on day one.

We validate this with a passive TLS handshake and certificate inspection, read-only, no exploitation. We report the expiry date, the SAN mismatch, or the weak negotiation as observed fact.

4. IDOR / BOLA: authorize the object, not just the user

Broken Object Level Authorization is the class that refuses to die, and this week was no exception. The application authenticates the user correctly, then fetches an object by an identifier the user supplies (/api/v2/invoices/48213) and checks only that the caller is logged in, never that the caller is entitled to that specific object.

Variants this cycle:

Defender checklist: check authorization on the tuple of (caller, object), on every object-scoped route, including the ones added last sprint; prefer authorization at the data layer so no handler can forget it; never treat an opaque identifier as an access-control boundary; and reject client-supplied owner/tenant fields on write.

We validate IDOR/BOLA the safe way: authenticate as one consented test principal, then attempt to access an object belonging to a second consented test principal we control. If the field-flip returns the second principal's data, that is the proof. We never reach into real customer data to demonstrate the gap: both sides of the test are accounts we own.

5. JWT algorithm confusion

The fifth class is JWT handling that trusts the token to describe how it should be verified. The classic move is alg: none: a token that declares it is unsigned, accepted by a verifier that honors the header. The subtler move is RS256-to-HS256 confusion: the verifier holds an RSA public key, the attacker re-signs the token with HS256 using that public key as the HMAC secret, and a naive library validates it because it picked the algorithm from the attacker-controlled header.

# The shape of the attack (illustrative, do not run against systems you don't own):
#   Original header:  {"alg":"RS256","typ":"JWT"}
#   Forged header:    {"alg":"HS256","typ":"JWT"}   <-- attacker downgrades
#   Forged signature: HMAC-SHA256(public_key_PEM, header.payload)
# If the verifier accepts it, the algorithm is taken from the token, not the policy.

Defender checklist: pin the accepted algorithm server-side and reject any token whose alg does not match; never let the token's header choose the verification path; reject alg: none outright; separate signing and verification key material by purpose; and validate iss, aud, and exp as non-negotiable, not optional.

We validate this by submitting our own test-issued tokens with a downgraded or none algorithm against an endpoint we are authorized to test, and observing whether the verifier accepts them. The finding is “the verifier honored an attacker-chosen algorithm,” demonstrated with a token we minted, never by forging access to a real identity.

The pattern behind the patterns

Five classes, one root cause: a security property that is true in the design document and skipped in the implementation. The login is supposed to throttle. The cookie is supposed to be HttpOnly. The cert is supposed to cover its host. The object is supposed to be authorized. The token is supposed to be verified the way policy says. Each gap is a place where the intended control quietly did not ship on one route, one host, one endpoint.

That is why a once-a-year point-in-time test misses them. These classes reappear on the next deploy, the next dependency bump, the next subdomain, the next route someone added on a Friday. The only defensible answer is to test the boundaries themselves, continuously, with bounded and consented probes that prove the gap without ever becoming the attack, and to attach the fix to every finding.

How Celvex Sentry tests for this

Each of these five classes maps to a family of probes that runs on every continuous-monitoring scan, not once a year. We measure auth-endpoint cost with bounded bursts against throwaway accounts, inspect every Set-Cookie across discovered hosts, handshake and read certificates on every enumerated subdomain, prove object-level authorization gaps with two principals we own, and submit our own downgraded tokens to test algorithm pinning. When a probe confirms a real gap, we mint a Proof Capsule with the exact evidence and a remediation attached. When the control is intact, we say so, with no false-positive noise.

Sources

Get your exposure check: full report in 4-24 hours

Full report in 4-24 hours. Real assessment on production-grade infrastructure. Paying customers get priority capacity.

Queue My Assessment