Summary
SpiderFoot collects OSINT data from many untrusted external sources during a scan (a crawled page's content, a server's version banner, WHOIS records, document metadata, hostnames of co-hosted/affiliated systems, and so on). When the correlation engine produces a result, it builds the correlation title by substituting these raw, attacker-influenceable event values into a rule headline template (for example "Cloud storage bucket found open: {data}"). That title is stored and later returned by the /scancorrelations JSON endpoint without any HTML escaping, and the scan view template injects it directly into the DOM with jQuery .append().
An attacker who controls a resource that SpiderFoot scans (their own server banner, WHOIS record for a domain that appears as an affiliate, a document served by the target, an open bucket name, etc.) can place an HTML/JavaScript payload in a field that ends up in a correlation title. When the operator opens the Correlations tab for that scan, the payload executes in the operator's browser. SpiderFoot ships with authentication disabled by default, and the operator's session can read the Settings page, which stores plaintext API keys for many third-party services. The XSS therefore allows exfiltration of those stored API keys and full control of the authenticated UI (start/stop/delete scans, change settings).
This is a sibling-sink to an existing server-side fix: the scan data element endpoint /scaneventresults does call html.escape() on the same event values (sfwebui.py line 1775), but the correlation endpoint and its render path were missed.
Details
- Correlation title is built from raw event data with no escaping.
spiderfoot/correlation.py, build_correlation_title() (lines 897-927):
title = rule['headline']
if isinstance(title, dict):
title = title['text']
fields = re.findall(r"{([a-z\.]+)}", title)
for m in fields:
try:
v = self.event_extract(data[0], m)[0]
except Exception:
self.log.error(f"Field requested was not available: {m}")
title = title.replace("{" + m + "}", v.replace("\r", "").split("\n")[0])
return title
v is a raw scan-event value (OSINT-collected). It is substituted into the title with no HTML escaping. create_correlation() (lines 929-956) then stores this title via correlationResultCreate().
38 of the shipped correlation rules embed raw event data into the headline, e.g.:
correlations/cloud_bucket_open.yaml: headline: "Cloud storage bucket found open: {data}"
correlations/open_port_version.yaml: headline: "Software version revealed on open port: {data}" (server version banner, attacker-controlled)
correlations/data_from_docmeta.yaml: headline: "Interesting data was found within document meta data: '{child.data}'"
correlations/human_name_in_whois.yaml / email_in_whois.yaml: WHOIS fields
correlations/http_errors.yaml: headline: "Multiple failure HTTP codes found at {entity.data}"
- The server returns the title unescaped.
sfwebui.py, scancorrelations() (lines 1722-1743):
for row in corrdata:
retdata.append([row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7]])
return retdata
row[1] is the correlation title. No html.escape() is applied to any column. Contrast the sibling endpoint scaneventresults() (lines 1771-1785), which DOES escape the same OSINT values:
retdata.append([
lastseen,
html.escape(row[1]), # <- data element value escaped here
html.escape(row[2]),
...
])
- The template injects the unescaped title into the DOM.
spiderfoot/templates/scaninfo.tmpl, browseCorrelations() (lines 459-492):
sf.fetchData('${docroot}/scancorrelations', {'id': instanceId}, function(data) {
...
for (var i = 0; i < data.length; i++) {
table += "<tr>";
table += "<td><a style='cursor: pointer' onClick='toggleCorrelation(\"" + instanceId + "\",\"" + data[i][0] + "\")'>" + data[i][1] + "</a> "; // line 471: data[i][1] = title, raw
table += "<i class='glyphicon glyphicon-question-sign' ... data-title=\"" + data[i][5] + "\"></i>"; // line 472
...
}
...
$("#mainbody").append(table); // line 492: DOM injection of raw HTML string
});
data[i][1] (the title) is concatenated straight into an HTML string and passed to jQuery .append(), which parses and inserts it as HTML. A title containing <img src=x onerror=...> becomes a live element and the onerror handler runs.
PoC
Steps (no special configuration required; default install, default no-auth):
-
Boot SpiderFoot: python sf.py -l 127.0.0.1:5001
-
An attacker arranges for a scanned resource to produce an event value containing an HTML payload. For example, with the open_port_version rule, the attacker runs a service whose banner is:
Apache/2.4<img src=x onerror=alert(document.cookie)>
or for cloud_bucket_open, an open bucket whose name is:
<img src=x onerror=alert(document.cookie)>evil-bucket
When SpiderFoot scans the attacker resource and the corresponding correlation rule fires, the correlation title becomes ... open: <img src=x onerror=alert(document.cookie)>evil-bucket.
-
The operator opens the scan and clicks the Correlations tab. The browser fetches /scancorrelations, which returns the title unescaped, and scaninfo.tmpl injects it via .append(). The onerror handler executes in the operator's session.
The full server path was confirmed end-to-end. Driving the real build_correlation_title(), the real DB layer, and the real running HTTP endpoint produced:
Server response from GET /scancorrelations?id=<scan>:
[["937e8c39...","Cloud storage bucket found open: <img src=x onerror=alert(document.cookie)>evil-bucket","cloud_bucket_open","HIGH","Open cloud bucket","An open cloud storage bucket was found.","id: cloud_bucket_open\n...",1]]
The same event value via GET /scaneventresults?id=<scan> is escaped (<img src=x onerror=alert(document.cookie)>evil-bucket), confirming the correlation path is the missed sink.
Feeding the real server response through the exact scaninfo.tmpl:471 concatenation yields the HTML string handed to jQuery .append():
<td><a ... onClick='toggleCorrelation("...","937e8c39...")'>Cloud storage bucket found open: <img src=x onerror=alert(document.cookie)>evil-bucket</a>...</td>
which contains a live <img src=x onerror=...> element.
Impact
Stored cross-site scripting. The attacker controls a resource that SpiderFoot collects data from (a server banner, an open bucket, a WHOIS record for a domain that shows up as an affiliate, document metadata served by the target, a hostname, etc.). The victim is the SpiderFoot operator who views the scan's correlations. Because SpiderFoot ships with authentication disabled by default and runs same-origin with the Settings page that stores plaintext third-party API keys, the executing JavaScript can read and exfiltrate those API keys and fully drive the authenticated UI (create, stop, delete scans, modify settings). The scope is changed (the injected data crosses from scan content into the operator's trusted UI origin), hence the elevated CVSS.
Affected Versions: 4.0.0 and earlier
Summary
SpiderFoot collects OSINT data from many untrusted external sources during a scan (a crawled page's content, a server's version banner, WHOIS records, document metadata, hostnames of co-hosted/affiliated systems, and so on). When the correlation engine produces a result, it builds the correlation title by substituting these raw, attacker-influenceable event values into a rule headline template (for example "Cloud storage bucket found open: {data}"). That title is stored and later returned by the
/scancorrelationsJSON endpoint without any HTML escaping, and the scan view template injects it directly into the DOM with jQuery.append().An attacker who controls a resource that SpiderFoot scans (their own server banner, WHOIS record for a domain that appears as an affiliate, a document served by the target, an open bucket name, etc.) can place an HTML/JavaScript payload in a field that ends up in a correlation title. When the operator opens the Correlations tab for that scan, the payload executes in the operator's browser. SpiderFoot ships with authentication disabled by default, and the operator's session can read the Settings page, which stores plaintext API keys for many third-party services. The XSS therefore allows exfiltration of those stored API keys and full control of the authenticated UI (start/stop/delete scans, change settings).
This is a sibling-sink to an existing server-side fix: the scan data element endpoint
/scaneventresultsdoes callhtml.escape()on the same event values (sfwebui.py line 1775), but the correlation endpoint and its render path were missed.Details
spiderfoot/correlation.py,build_correlation_title()(lines 897-927):vis a raw scan-event value (OSINT-collected). It is substituted into the title with no HTML escaping.create_correlation()(lines 929-956) then stores this title viacorrelationResultCreate().38 of the shipped correlation rules embed raw event data into the headline, e.g.:
correlations/cloud_bucket_open.yaml:headline: "Cloud storage bucket found open: {data}"correlations/open_port_version.yaml:headline: "Software version revealed on open port: {data}"(server version banner, attacker-controlled)correlations/data_from_docmeta.yaml:headline: "Interesting data was found within document meta data: '{child.data}'"correlations/human_name_in_whois.yaml/email_in_whois.yaml: WHOIS fieldscorrelations/http_errors.yaml:headline: "Multiple failure HTTP codes found at {entity.data}"sfwebui.py,scancorrelations()(lines 1722-1743):row[1]is the correlation title. Nohtml.escape()is applied to any column. Contrast the sibling endpointscaneventresults()(lines 1771-1785), which DOES escape the same OSINT values:spiderfoot/templates/scaninfo.tmpl,browseCorrelations()(lines 459-492):data[i][1](the title) is concatenated straight into an HTML string and passed to jQuery.append(), which parses and inserts it as HTML. A title containing<img src=x onerror=...>becomes a live element and theonerrorhandler runs.PoC
Steps (no special configuration required; default install, default no-auth):
Boot SpiderFoot:
python sf.py -l 127.0.0.1:5001An attacker arranges for a scanned resource to produce an event value containing an HTML payload. For example, with the
open_port_versionrule, the attacker runs a service whose banner is:Apache/2.4<img src=x onerror=alert(document.cookie)>or for
cloud_bucket_open, an open bucket whose name is:<img src=x onerror=alert(document.cookie)>evil-bucketWhen SpiderFoot scans the attacker resource and the corresponding correlation rule fires, the correlation title becomes
... open: <img src=x onerror=alert(document.cookie)>evil-bucket.The operator opens the scan and clicks the Correlations tab. The browser fetches
/scancorrelations, which returns the title unescaped, andscaninfo.tmplinjects it via.append(). Theonerrorhandler executes in the operator's session.The full server path was confirmed end-to-end. Driving the real
build_correlation_title(), the real DB layer, and the real running HTTP endpoint produced:Server response from
GET /scancorrelations?id=<scan>:The same event value via
GET /scaneventresults?id=<scan>is escaped (<img src=x onerror=alert(document.cookie)>evil-bucket), confirming the correlation path is the missed sink.Feeding the real server response through the exact
scaninfo.tmpl:471concatenation yields the HTML string handed to jQuery.append():which contains a live
<img src=x onerror=...>element.Impact
Stored cross-site scripting. The attacker controls a resource that SpiderFoot collects data from (a server banner, an open bucket, a WHOIS record for a domain that shows up as an affiliate, document metadata served by the target, a hostname, etc.). The victim is the SpiderFoot operator who views the scan's correlations. Because SpiderFoot ships with authentication disabled by default and runs same-origin with the Settings page that stores plaintext third-party API keys, the executing JavaScript can read and exfiltrate those API keys and fully drive the authenticated UI (create, stop, delete scans, modify settings). The scope is changed (the injected data crosses from scan content into the operator's trusted UI origin), hence the elevated CVSS.
Affected Versions: 4.0.0 and earlier