~/blog/downgrade-attack-tls-version-enforcementPOSTED
by Shady Nathan Tawfik · August 15, 2026 · 4 min

If TLS Enforcement Is Optional, Version Downgrades Become Boring

A university content platform served mixed protocol versions on some hosts. No cipher was weak, no certificate was expired, and the transport layer still failed the assessment.

Interactive[blog-2.0]

TL;DR: No weak cipher, no expired certificate — and the transport layer still failed. I verified protocol enforcement per hostname, found the archive host negotiating older versions, and set one estate-wide baseline.

The scenario

I ran this as a black-box review: no credentials, only the publicly visible surface. Here is what I saw before I touched anything.

A university content platform on an enterprise content system with a campus-wide service catalogue, distributed across multiple hosts including an archive. Assessment covered transport security configuration across every host, mail authentication, header policy and internal host exposure.

I removed all identifying information. The target is described only by sector and technology class so the pattern can be reused.

The pattern

The pattern I recognised — and that I keep finding in similar estates:

Transport security is the one layer where a downgrade is invisible in a screenshot. A protocol negotiation that falls back to an older version succeeds, the padlock appears, the certificate validates, and the page loads normally. The user has no signal at all. This is why protocol enforcement has to be verified from the outside, per host, rather than assumed from the fact that one host is configured correctly.

The cause is almost always configuration scope. An administrator secures the primary host, verifies it, and stops. Archive hosts, preview environments and service subdomains inherit the platform defaults, and the defaults on a content system are frequently permissive because the vendor supports a wider range of deployments than any single site needs. Each of these is a separate hostname with a separate certificate and a separate configuration file, and nothing reminds anyone that they exist.

What makes it worth a finding rather than a note is the combination with everything else. A site that enforces protocol versions on two hosts and not on three has an inconsistent perimeter, and an inconsistent perimeter is where an attacker chooses where to stand. The archive host with the older protocol negotiation is the one that gets used, not because it is attractive but because it is where the negotiation is easiest.

TL;DR: A university content platform served mixed protocol versions on some hosts. No cipher was weak, no certificate was expired, and the transport layer still failed the assessment.

Findings

SeverityFindingEvidence
MEDIUMOlder transport versions accepted on archive hostsDowngrade available on hosts the primary does not allow
MEDIUMMail authentication not enforced for the sending domainSpoofable university mail
MEDIUMInternal host names exposed in response headersService naming visible to the internet
LOWProtocol enforcement inconsistent between hostsNo estate-wide baseline
LOWStrict transport security without preloadPolicy present but not submitted for preload
INFOCertificates valid and key sizes adequateNo certificate-level finding

The attack path

Client offers modern and older protocolsPrimary host negotiates modernArchive host negotiates olderDowngrade to a weaker suiteInconsistent perimeter across the estate

Reproduction in my lab

Every command below targets a lab container I control. Nothing here is aimed at a live system.

Test protocol enforcement per host on your own estate

~/code/bash bash
# do every hosts, not just the primary
$ for h in example.org www.example.org archive.example.org; do
    printf '%-26s ' "$h"
    curl -so /dev/null -w '%{http_version} %{ssl_verify_result}\n' --tlsv1.0 --tls-max 1.0 "https://$h/" 2>&1 | tail -1
  done

# and what the strictest host actually negotiated
$ curl -sv https://example.org/ 2>&1 | grep -iE 'SSL connection using|TLSv1'

Detection in your own estate

Enumerate every hostname that serves content

~/code/bash bash
$ for h in $(cat hosts.txt); do
    printf '%s ' "$h"
    openssl s_client -connect "$h:443" -servername "$h" </dev/null 2>/dev/null \
      | openssl x509 -noout -subject -dates 2>/dev/null | tr '\n' ' '
    echo
done

Remediation

Set one transport baseline for the whole estate

~/code/nginx nginx
# estate-wide baseline: modern versions only, forward secrecy only
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;

# apply it everywhere, including archive and preview hosts
include /etc/nginx/tls-baseline.conf;

Once an attacker gains access, they can perform the following actions:

Older transport versions accepted on archive hosts
Mail authentication not enforced for the sending domain
Internal host names exposed in response headers

Takeaways

  • Verify transport configuration per hostname; a screenshot proves nothing about negotiation.
  • Archive and preview hosts are the ones that miss the baseline.
  • An inconsistent perimeter is where an attacker chooses where to stand.
  • Preload requires explicit submission; the header alone does nothing extra.

For educational purposes only. Every reproducible command targets my own lab environment (localhost), never a live system. © 2026