Like a valet tagging your keys as private but forgetting to put them in the locked drawer
This maps to CVE-2023-28708. In Apache Tomcat 9.0.0-M1 through 9.0.71, when RemoteIpFilter is used behind a reverse proxy and requests arrive over HTTP with X-Forwarded-Proto: https, Tomcat can issue session cookies without the Secure attribute. The result is not code execution on the server; it is a session-cookie transport weakness that can let a browser send JSESSIONID over an insecure channel.
The vendor MEDIUM label is fair in a vacuum, but it is a little generous for enterprise patch triage. Real exploitation needs a very specific deployment pattern and a second condition on the victim side: the attacker must either observe or induce an HTTP request path where that cookie is exposed. That compounding friction pushes this down to LOW for most fleets.
4 steps from start to impact.
Find the narrow deployment pattern
RemoteIpFilter is actually enabled and the app sits behind a reverse proxy that forwards X-Forwarded-Proto. Typical tooling here is Nessus plugin 173251 for version detection plus Burp Suite or curl to inspect headers and cookie behavior. Version alone is not enough; reachability depends on config.- Apache Tomcat 9.0.0-M1 through 9.0.71
RemoteIpFilterenabled in Tomcat or app config- Reverse proxy forwards
X-Forwarded-Proto - Proxy-to-Tomcat traffic reaches Tomcat over HTTP
- Many Tomcat instances do not use
RemoteIpFilterat all - Some environments terminate TLS differently or set cookie flags at the proxy
- Internet-facing banners do not prove this exact configuration
Confirm insecure cookie issuance
curl, or browser dev tools, the attacker requests the application through the proxy and checks Set-Cookie for JSESSIONID without the Secure flag. This confirms the bug is live in that path. No server memory corruption or exploit payload is involved; this is pure response-side misconfiguration behavior.- Application issues a session cookie on the tested path
- Attacker can reach the app through the same reverse-proxy route users take
- Some apps or proxies rewrite cookies and add
Securebefore they hit the browser - Stateless apps or paths that do not issue a session cookie give the attacker nothing to steal
Create or observe an HTTP leak path
- Victim browser has the affected session cookie
- There is a reachable HTTP path for the same origin or an on-path adversary can observe/inject traffic
- User interaction or user browsing activity occurs
- HSTS and HTTPS-only redirects sharply reduce practical exploitation
- Modern enterprises often block plaintext access at the edge
- Captive-portal or hostile-Wi-Fi style positions are not universal attacker positions
Replay the session cookie
JSESSIONID, they can replay it with Burp Repeater or curl to impersonate the victim session. Impact is typically limited to the victim's web session and whatever that account can do. This is account/session hijack risk, not host takeover.- Captured session cookie remains valid
- Application does not strongly bind the session to IP, device, or step-up auth
- Short session lifetimes can expire the window
- MFA at re-auth or privileged action steps can contain blast radius
- Session binding and anomaly detection can break replay
The supporting signals.
| Primary mapping | Tenable plugin 173251 maps to CVE-2023-28708 for Tomcat 9.x, with the plugin explicitly calling out Tomcat 9.0.0.M1 < 9.0.72. |
|---|---|
| In-the-wild status | No strong public evidence of active exploitation found. CERT Santé marks active exploitation as No, and I found no CISA KEV listing or campaign reporting tied to this CVE. |
| Public proof-of-concept | No meaningful public exploit tooling signal. CERT Santé says no open-source proof of concept is available; this fits the bug class because exploitation is mostly header/cookie validation plus traffic interception, not a flashy RCE chain. |
| EPSS | Low exploit prediction. A FIRST-fed snapshot surfaced by Wiz shows roughly 0.10% EPSS and about the 27.7th percentile, which is consistent with a niche, low-weaponization bug. |
| KEV status | Not in CISA KEV. No KEV due date, no federal emergency patch driver, and no public evidence that CISA considers this one actively exploited at scale. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N = remote reachability, but only low confidentiality impact, with user interaction required and no integrity/availability effect. |
| Affected range | Tomcat 9.0.0-M1 through 9.0.71 are affected in the 9.x line. Upstream also lists 8.5.0-8.5.85, 10.1.0-M1-10.1.5, and 11.0.0-M1-M2. |
| Fixed versions and backports | Upstream fix is 9.0.72. Distro backports exist, including Ubuntu 22.04 9.0.58-1ubuntu0.1+esm4, Ubuntu 20.04 9.0.31-1ubuntu0.8, Debian backports for tomcat9, and SUSE package updates such as tomcat >= 9.0.36-3.102.1 on SLES 12 SP5. |
| Exposure reality | No reliable internet-wide census for this CVE specifically. Shodan/Censys-style Tomcat counts overstate the population because this bug is not banner-detectable; it only matters when RemoteIpFilter is enabled behind a proxy and the browser still has some HTTP path to the app. |
| Disclosure and reporting | Upstream says the underlying bug was reported publicly on 2023-02-08 via Apache Bugzilla 66471; the Tomcat Security Team identified the security implications on 2023-02-09 and made the issue public on 2023-03-22. |
noisgate verdict.
The decisive factor is that this bug needs a specific reverse-proxy + RemoteIpFilter deployment pattern and then still needs a victim-side HTTP leakage path before an attacker gets value. That makes it materially less urgent than the vendor baseline suggests for a 10,000-host patch queue, because the reachable population and the blast radius are both narrow.
Why this verdict
- Downgrade from vendor MEDIUM: the vendor baseline assumes a vulnerable version matters on its own, but this CVE is only reachable when
RemoteIpFilteris enabled behind a proxy that forwardsX-Forwarded-Protoover an HTTP backend leg. - Second-stage friction matters: exploitation still needs a browser-side HTTP leak or an on-path attacker. If you already enforce HTTPS-only with HSTS and no plaintext access to the app origin, the practical path collapses fast.
- Blast radius is session-level, not server-level: the outcome is stolen
JSESSIONIDand session replay, not RCE, not arbitrary file read, and not a universal auth bypass across the server.
Why not higher?
There is no server compromise here by itself. No CISA KEV listing, no strong public exploitation signal, no public PoC momentum, and the chain depends on both a narrow config pattern and a victim-side transport weakness; that is not how HIGH or CRITICAL patch drivers behave in real fleets.
Why not lower?
It is still a real security defect in a common platform, and plenty of enterprises do run Tomcat behind reverse proxies. If the application still exposes HTTP anywhere on the same origin, a stolen session cookie can become immediate account hijack for authenticated users, so this is not pure noise.
What to do — in priority order.
- Force
Secureat the edge — Configure the reverse proxy or load balancer to append theSecureattribute toJSESSIONID(and ideallyHttpOnlyandSameSite) where application behavior allows it. For a LOW verdict there is no fixed SLA; treat this as backlog hygiene, but do it quickly if the edge rule is low-risk and easy. - Kill plaintext access — Enforce HTTPS-only, redirect port 80 immediately, and enable HSTS on the public origin so the browser never has a practical HTTP path to send the cookie. For LOW, there is no mitigation SLA; this is still the best compensating control because it removes the attacker’s key friction bypass.
- Inventory
RemoteIpFilterusage — Find Tomcat instances whereorg.apache.catalina.filters.RemoteIpFilteris configured inconf/or appweb.xmlfiles, and prioritize only those systems for upgrade validation. For LOW, there is no fixed SLA; use this to trim patch scope instead of blindly burning change windows on every Tomcat host. - Hunt for authenticated HTTP traffic — Search proxy and web logs for HTTP requests to domains that should be HTTPS-only, especially authenticated routes or paths that set
JSESSIONID. For LOW, there is no fixed SLA; use this as a quick exposure check before maintenance windows.
HttpOnlyalone does not fix this; it stops JavaScript reads, not browser transmission over HTTP.- Encrypting only the proxy-to-Tomcat backend leg does not solve the browser-side cookie leak if the site is still reachable over HTTP.
- A generic WAF signature is weak here because the issue is missing cookie metadata and session replay, not a distinctive exploit payload.
Crowdsourced verification payload.
Run this on the target Tomcat host or from a mounted filesystem copy of the Tomcat install. Invoke it as python3 verify_cve_2023_28708.py /opt/tomcat and use root only if you need to read all app WEB-INF/web.xml files; otherwise the Tomcat service account is usually enough.
#!/usr/bin/env python3
"""
verify_cve_2023_28708.py
Checks whether a Tomcat 9 installation is vulnerable to CVE-2023-28708.
Logic:
- If Tomcat version is 9.0.72 or later => PATCHED
- If Tomcat version is 9.0.0-M1 through 9.0.71 and RemoteIpFilter is found in common config/app files => VULNERABLE
- If version is vulnerable but RemoteIpFilter is not found => UNKNOWN
- If version cannot be determined => UNKNOWN
Exit codes:
0 = PATCHED
1 = VULNERABLE
2 = UNKNOWN
"""
import os
import re
import sys
import zipfile
from pathlib import Path
REMOTE_IP_PATTERNS = [
"RemoteIpFilter",
"org.apache.catalina.filters.RemoteIpFilter",
]
def read_text(path):
try:
return Path(path).read_text(errors="ignore")
except Exception:
return None
def get_version_from_release_notes(base):
for rel in [base / "RELEASE-NOTES", base / "RUNNING.txt", base / "RELEASE-NOTES.txt"]:
txt = read_text(rel)
if not txt:
continue
m = re.search(r"Apache Tomcat Version\s+([0-9]+\.[0-9]+\.[0-9]+(?:[-.][A-Za-z0-9]+)?)", txt)
if m:
return m.group(1)
m = re.search(r"Apache Tomcat/([0-9]+\.[0-9]+\.[0-9]+(?:[-.][A-Za-z0-9]+)?)", txt)
if m:
return m.group(1)
return None
def get_version_from_manifest(base):
jar_candidates = [
base / "lib" / "catalina.jar",
base / "lib" / "tomcat-catalina.jar",
]
for jar in jar_candidates:
if not jar.exists():
continue
try:
with zipfile.ZipFile(jar, "r") as zf:
for name in ["META-INF/MANIFEST.MF", "org/apache/catalina/util/ServerInfo.properties"]:
try:
data = zf.read(name).decode("utf-8", errors="ignore")
except Exception:
continue
m = re.search(r"(?:Implementation-Version|Server number):\s*([0-9]+\.[0-9]+\.[0-9]+(?:[-.][A-Za-z0-9]+)?)", data)
if m:
return m.group(1)
except Exception:
pass
return None
def normalize_version(v):
# Supports 9.0.71 and 9.0.0-M1 style values.
# milestone/build suffixes sort below final releases.
if not v:
return None
m = re.match(r"^(\d+)\.(\d+)\.(\d+)(?:[-.]?([A-Za-z]+)(\d+)?)?$", v)
if not m:
return None
major, minor, patch = int(m.group(1)), int(m.group(2)), int(m.group(3))
label = (m.group(4) or "").upper()
label_num = int(m.group(5) or 0)
order = {"M": -3, "MILESTONE": -3, "RC": -2, "B": -1, "": 0}
return (major, minor, patch, order.get(label, 0), label_num)
def version_lt(a, b):
na, nb = normalize_version(a), normalize_version(b)
if na is None or nb is None:
return None
return na < nb
def version_is_tomcat9(v):
nv = normalize_version(v)
return nv is not None and nv[0] == 9
def find_remote_ip_filter(base):
candidates = []
conf = base / "conf"
if conf.exists():
candidates.extend(conf.rglob("*.xml"))
webapps = base / "webapps"
if webapps.exists():
candidates.extend(webapps.rglob("WEB-INF/web.xml"))
hits = []
for path in candidates:
try:
txt = path.read_text(errors="ignore")
except Exception:
continue
if any(p in txt for p in REMOTE_IP_PATTERNS):
hits.append(str(path))
return hits
def main():
if len(sys.argv) != 2:
print("UNKNOWN - usage: python3 verify_cve_2023_28708.py /path/to/tomcat")
sys.exit(2)
base = Path(sys.argv[1]).resolve()
if not base.exists() or not base.is_dir():
print(f"UNKNOWN - Tomcat base not found: {base}")
sys.exit(2)
version = get_version_from_release_notes(base) or get_version_from_manifest(base)
if not version:
print("UNKNOWN - could not determine Tomcat version")
sys.exit(2)
if not version_is_tomcat9(version):
print(f"UNKNOWN - detected Tomcat version {version}; this script only assesses the Tomcat 9 plugin scope")
sys.exit(2)
cmp_res = version_lt(version, "9.0.72")
if cmp_res is None:
print(f"UNKNOWN - could not compare version {version} to 9.0.72")
sys.exit(2)
if not cmp_res:
print(f"PATCHED - Tomcat {version} is not in the vulnerable 9.x range for CVE-2023-28708")
sys.exit(0)
hits = find_remote_ip_filter(base)
if hits:
print(f"VULNERABLE - Tomcat {version} is < 9.0.72 and RemoteIpFilter was found in: {', '.join(hits[:10])}")
sys.exit(1)
print(f"UNKNOWN - Tomcat {version} is < 9.0.72 but RemoteIpFilter was not found in common configs; manual review of proxy/cookie behavior is required")
sys.exit(2)
if __name__ == "__main__":
main()
If you remember one thing.
RemoteIpFilter behind a reverse proxy and confirm whether the public origin still permits any HTTP path; for a LOW verdict, the noisgate mitigation SLA does not apply and the noisgate remediation SLA has no fixed deadline either, so treat it as backlog hygiene. If you can add Secure at the edge or shut off plaintext access with a low-risk proxy change, do that during normal operations; otherwise roll the upgrade to 9.0.72+ in routine maintenance after higher-value internet-facing flaws are cleared.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.