This is a loaded nail gun left on the bench, not a burglar walking through the front door
Tenable plugin 111066 maps this finding to CVE-2018-8014, an Apache Tomcat CORS filter insecure-defaults issue. Upstream says the actual affected Tomcat 7 range is 7.0.41 through 7.0.88, fixed in code for 7.0.89 but practically consumed via the next public 7.x release 7.0.90+; Tenable's plugin title 7.0.0 < 7.0.89 is a broader version check, not proof that the vulnerable behavior exists on-host. The bug matters only if an application uses org.apache.catalina.filters.CorsFilter and leaves the default behavior in place so credentialed cross-origin requests are effectively allowed.
Vendor-style high/critical scoring does not match enterprise reality here. Apache labels it Low and explicitly says most users should not be impacted; that matches the real attack chain, which requires an optional filter, unsafe/default app configuration, and usually a logged-in victim browser to visit attacker content before any data can be read.
4 steps from start to impact.
Find a Tomcat app that actually uses CorsFilter
web.xml maps org.apache.catalina.filters.CorsFilter. A plain Tomcat 7 banner is not enough; this is not a raw server-side memory corruption or request parser bug. Usual validation tools are curl, browser dev tools, or Burp Suite against endpoints that return Access-Control-* headers.- Tomcat 7 is in the affected upstream range
- An app or global config has enabled
org.apache.catalina.filters.CorsFilter - The filter is reachable on sensitive endpoints
- Many Tomcat deployments do not use the built-in CORS filter at all
- Version scans cannot prove the filter is enabled
- Even when enabled, many apps override the defaults with explicit origin allowlists
111066 is version-based and will over-report compared with real exploitability; config review or authenticated file inspection is needed.Rely on unsafe defaults instead of explicit CORS policy
cors.support.credentials=false or tightly scope cors.allowed.origins, the practical issue collapses. Burp Repeater or a browser fetch() from an attacker origin is enough to test this.- Filter is configured with defaults or equivalently unsafe settings
- Target endpoints return sensitive authenticated data
- Secure app teams often already pin origins for SPA/API use
- Some sensitive responses are still blocked by app-layer authz or anti-CSRF logic
- Modern cookie settings may suppress cross-site credentials
Access-Control-Allow-Credentials: true combined with permissive origin handling in proxy logs, browser traces, or application security tests.Get a logged-in browser to make the request
UI:N story breaks from reality. In practice, the attacker commonly needs a victim already authenticated to the target app and then needs that user to load attacker-controlled JavaScript or content that triggers credentialed cross-origin requests. Tooling is trivial: any hosted HTML/JS page can do it.- Victim has an active authenticated session
- Victim browser is allowed to send relevant cookies cross-site
- Attacker can induce the victim to visit attacker content
- SameSite cookies may block the cross-site credential flow
- Users are not always concurrently logged in
- This is browser-mediated abuse, not direct server compromise
Origin headers, especially external domains paired with authenticated API calls.Read cross-origin data through the victim session
- Target response contains useful sensitive data
- Browser accepts and exposes the response to attacker JavaScript
- Impact is limited to what the victim can access
- No shell, no service account compromise, no direct server-side persistence from this flaw alone
- Per-user scoping sharply narrows blast radius
The supporting signals.
| In-the-wild status | No confirmed active exploitation found in reviewed sources, and no CISA KEV listing was found. That absence is an inference from the current KEV catalog, not proof exploitation never occurred. |
|---|---|
| Proof-of-concept availability | Trivial to validate, but not a weaponized RCE exploit class. A browser fetch() or Burp request from a foreign Origin is enough. Tenable's CVE page currently marks Exploit available: False for its plugin coverage. |
| EPSS | Tenable's CVE page shows EPSS 0.61177 for CVE-2018-8014. Treat that as noisy here: EPSS is CVE-level, while the exploitable state depends on app-specific CORS usage and browser/session conditions. |
| KEV status | Not listed in the reviewed CISA KEV catalog. No KEV due date applies. |
| CVSS vector | NVD still carries CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8 Critical. That vector materially overstates reality because the real chain usually requires a victim browser, authenticated session state, and an optional filter left on defaults. |
| Affected versions | Upstream Apache says Tomcat 7 7.0.41 to 7.0.88 is affected for this CVE. Tenable plugin 111066 uses the broader title 7.0.0 < 7.0.89, so expect false urgency from pure banner/version findings. |
| Fixed versions | Apache says the fix landed in 7.0.89, but that release was not publicly shipped; for real 7.x remediation, use 7.0.90+. Distro backports exist: Ubuntu shows tomcat7 on 14.04 fixed at 7.0.52-1ubuntu0.14 and tomcat8 fixes such as 8.5.30-1ubuntu1.2 on 18.04. |
| Scanner / detection coverage | Coverage is good for version discovery, weak for exploitability. Nessus 111066 flags upstream-like version ranges; it does not prove CorsFilter is enabled or insecurely configured. |
| Exposure data | No reliable internet census was found in reviewed sources for this specific condition. Search engines can find exposed Tomcat services, but they cannot tell from banners whether a given app uses CorsFilter with unsafe defaults. |
| Disclosure / reporter | Published 2018-05-16. Apache's Tomcat 7 advisory does not prominently credit an external reporter for this specific item in the reviewed page, unlike many of its other entries. |
noisgate verdict.
The single biggest downgrader is that this is not directly reachable as a server compromise primitive; it only becomes exploitable when an optional CORS filter is enabled and left on unsafe defaults. That sharply narrows the affected population and turns the impact into browser-mediated data exposure, not broad host compromise.
Why this verdict
- Down from NVD 9.8 because attacker position is not truly unauthenticated-direct. The chain usually requires a victim browser plus authenticated session state, even though NVD scored it
UI:N. - Down again because exposure population is small. Only Tomcat deployments that actually enable
org.apache.catalina.filters.CorsFilterand leave defaults or equivalent unsafe settings are meaningfully exposed. - Down again because blast radius is per-user and app-scoped. Successful abuse reads what the victim can access; it does not hand over the Tomcat host, service account, or tenant-wide control by itself.
Why not higher?
There is no clean server-side takeover path here. No active exploitation evidence surfaced, no KEV listing exists, and the vulnerable state depends on optional feature usage plus weak application configuration. That is a long way from the kind of internet-wormable or one-packet pre-auth control loss that earns HIGH or CRITICAL in real operations.
Why not lower?
This is still a real security defect, not pure scanner noise. If a sensitive internal portal or internet-facing app uses the Tomcat CORS filter with defaults, an attacker may be able to turn a victim's browser into a cross-origin data siphon. That justifies keeping it above IGNORE.
What to do — in priority order.
- Audit
CorsFilterusage — Search global and applicationweb.xmlfiles fororg.apache.catalina.filters.CorsFilterand confirm whether defaults are being relied on. For a LOW verdict there is no SLA (treat as backlog hygiene), so do this during the next application hardening cycle rather than as an emergency. - Pin allowed origins explicitly — Set
cors.allowed.originsto an exact allowlist and avoid permissive origin reflection. This removes the dangerous default posture; for LOW, there is no mitigation SLA, so implement in the normal release train. - Disable credentialed CORS unless truly required — Set
cors.support.credentials=falseanywhere cross-origin credentials are not a hard business requirement. This sharply reduces browser-mediated abuse paths; for LOW, handle as backlog hygiene. - Review cookie policy — Use stronger
SameSiteand secure cookie settings where compatible, because they can suppress the cross-site credential send that this attack depends on. This is a defense-in-depth control, not a substitute for fixing the CORS policy, and for LOW there is no mitigation SLA.
- A network firewall alone does not solve this if legitimate users can already reach the app; the abuse rides the victim browser and its authenticated session.
- EDR on the Tomcat host is not a strong control here because the server behaves as configured; there may be no process injection, shell spawn, or obvious host artifact.
- A version-only exception process is dangerous in both directions: it may overstate urgent patch risk, but it also misses the real question, which is whether
CorsFilteris enabled and unsafe.
Crowdsourced verification payload.
Run this on the Tomcat host or from a mounted application filesystem. Invoke it with python3 tomcat_cve_2018_8014_check.py /opt/tomcat or python tomcat_cve_2018_8014_check.py "C:\Tomcat". It needs read access only to the Tomcat install and deployed app web.xml files; admin privileges are usually not required unless the filesystem is locked down.
#!/usr/bin/env python3
# Check practical exposure to CVE-2018-8014 on Apache Tomcat.
# Outputs one of: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 1 vulnerable, 0 patched, 2 unknown
import os
import re
import sys
import zipfile
import xml.etree.ElementTree as ET
NS = ''
def norm_tag(tag):
if '}' in tag:
return tag.split('}', 1)[1]
return tag
def parse_version_from_text(text):
m = re.search(r'Apache Tomcat/?\s*([0-9]+\.[0-9]+\.[0-9]+)', text, re.I)
if m:
return m.group(1)
m = re.search(r'([0-9]+\.[0-9]+\.[0-9]+)', text)
if m:
return m.group(1)
return None
def version_tuple(v):
try:
return tuple(int(x) for x in v.split('.'))
except Exception:
return None
def get_tomcat_version(base):
candidates = [
os.path.join(base, 'RELEASE-NOTES'),
os.path.join(base, 'RUNNING.txt'),
os.path.join(base, 'lib', 'catalina.jar'),
]
for path in candidates:
if not os.path.exists(path):
continue
try:
if path.endswith('.jar'):
with zipfile.ZipFile(path, 'r') as zf:
for name in ('META-INF/MANIFEST.MF', 'org/apache/catalina/util/ServerInfo.properties'):
try:
data = zf.read(name).decode('utf-8', 'ignore')
v = parse_version_from_text(data)
if v:
return v
except Exception:
pass
else:
with open(path, 'r', encoding='utf-8', errors='ignore') as fh:
data = fh.read(4096)
v = parse_version_from_text(data)
if v:
return v
except Exception:
pass
return None
def parse_webxml(path):
try:
tree = ET.parse(path)
root = tree.getroot()
except Exception:
return []
filters = {}
mappings = set()
for elem in root.iter():
tag = norm_tag(elem.tag)
if tag == 'filter':
fname = None
fclass = None
params = {}
for child in list(elem):
ctag = norm_tag(child.tag)
text = (child.text or '').strip()
if ctag == 'filter-name':
fname = text
elif ctag == 'filter-class':
fclass = text
elif ctag == 'init-param':
pname = None
pvalue = None
for p in list(child):
ptag = norm_tag(p.tag)
ptext = (p.text or '').strip()
if ptag == 'param-name':
pname = ptext
elif ptag == 'param-value':
pvalue = ptext
if pname:
params[pname] = pvalue or ''
if fname:
filters[fname] = {'class': fclass or '', 'params': params}
elif tag == 'filter-mapping':
fname = None
for child in list(elem):
if norm_tag(child.tag) == 'filter-name':
fname = (child.text or '').strip()
if fname:
mappings.add(fname)
findings = []
for fname, meta in filters.items():
if meta['class'] == 'org.apache.catalina.filters.CorsFilter' and fname in mappings:
params = {k.lower(): v for k, v in meta['params'].items()}
allowed = params.get('cors.allowed.origins', '').strip()
creds = params.get('cors.support.credentials', '').strip().lower()
# Practical exposure heuristic:
# - missing cors.support.credentials => default used
# - missing/empty/* allowed origins => permissive default or equivalent
creds_default_or_true = (creds == '' or creds == 'true')
origins_default_or_wild = (allowed == '' or allowed == '*')
explicitly_safer = (creds == 'false') or (allowed not in ('', '*'))
findings.append({
'file': path,
'filter_name': fname,
'creds': creds if creds else '(default)',
'allowed': allowed if allowed else '(default)',
'practically_vulnerable': creds_default_or_true and origins_default_or_wild and not explicitly_safer,
})
return findings
def collect_webxmls(base):
results = []
for root, dirs, files in os.walk(base):
# Avoid huge vendor trees that are unlikely to hold deployed descriptors
if any(skip in root for skip in (os.sep + '.git', os.sep + 'work' + os.sep)):
continue
for f in files:
if f.lower() == 'web.xml':
results.append(os.path.join(root, f))
return results
def main():
if len(sys.argv) != 2:
print('UNKNOWN - usage: python3 tomcat_cve_2018_8014_check.py <TOMCAT_HOME>')
sys.exit(2)
base = sys.argv[1]
if not os.path.isdir(base):
print('UNKNOWN - base path not found: %s' % base)
sys.exit(2)
version = get_tomcat_version(base)
if not version:
print('UNKNOWN - could not determine Tomcat version from %s' % base)
sys.exit(2)
vt = version_tuple(version)
if not vt:
print('UNKNOWN - unparsable Tomcat version: %s' % version)
sys.exit(2)
vulnerable_range = (version_tuple('7.0.41') <= vt <= version_tuple('7.0.88'))
patched_upstream = vt >= version_tuple('7.0.90')
if patched_upstream:
print('PATCHED - Tomcat version %s is at or above the first public fixed 7.x release (7.0.90)' % version)
sys.exit(0)
if not vulnerable_range:
print('PATCHED - Tomcat version %s is outside the upstream vulnerable Tomcat 7 range for CVE-2018-8014 (7.0.41-7.0.88)' % version)
sys.exit(0)
webxmls = collect_webxmls(base)
if not webxmls:
print('UNKNOWN - Tomcat %s is in the vulnerable range, but no web.xml files were found to validate CorsFilter usage' % version)
sys.exit(2)
findings = []
for path in webxmls:
findings.extend(parse_webxml(path))
if not findings:
print('PATCHED - Tomcat %s is in the vulnerable version range, but no mapped org.apache.catalina.filters.CorsFilter was found' % version)
sys.exit(0)
bad = [f for f in findings if f['practically_vulnerable']]
if bad:
print('VULNERABLE - Tomcat %s is in range and CorsFilter appears to rely on insecure defaults/equivalent settings' % version)
for f in bad:
print(' file=%s filter=%s cors.support.credentials=%s cors.allowed.origins=%s' % (f['file'], f['filter_name'], f['creds'], f['allowed']))
sys.exit(1)
print('PATCHED - Tomcat %s is in the vulnerable version range, but discovered CorsFilter configs appear explicitly constrained' % version)
for f in findings:
print(' file=%s filter=%s cors.support.credentials=%s cors.allowed.origins=%s' % (f['file'], f['filter_name'], f['creds'], f['allowed']))
sys.exit(0)
if __name__ == '__main__':
main()
If you remember one thing.
tenable:111066 hit to determine whether the host is really in the upstream affected range (7.0.41-7.0.88) and whether any deployed app actually uses CorsFilter with unsafe defaults; if not, close or downgrade the finding. Because this lands LOW, there is no noisgate mitigation SLA and noisgate remediation SLA is effectively backlog hygiene, so fold the Tomcat upgrade and any CORS policy cleanup into the normal release train rather than burning an out-of-band patch window.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.