This document describes the security architecture for Irgo desktop applications.
Desktop Irgo applications run a local HTTP server on 127.0.0.1 (localhost) to serve the application UI. While this server only binds to the loopback interface and is not accessible from other machines, it faces several potential threats:
A malicious website could use DNS rebinding to make requests to your localhost server:
- User visits
malicious.comwhich initially resolves to attacker's IP - Attacker's DNS then rebinds to
127.0.0.1 - JavaScript on the page can now make requests to your local server
Any website the user visits can attempt to make requests to known localhost ports:
- Forms can POST to
http://127.0.0.1:PORT/endpoint - Images/scripts can trigger GET requests
- JavaScript can make fetch/XHR requests (limited by CORS)
Browser extensions with appropriate permissions can:
- Read and modify requests to localhost
- Bypass CORS restrictions
- Access cookies and local storage
Malicious websites can probe localhost ports to discover running services and potentially fingerprint applications.
The server only binds to 127.0.0.1, never to 0.0.0.0 or any external interface. This ensures the server is not accessible from the network.
server.Addr = fmt.Sprintf("127.0.0.1:%d", port)Every application launch generates a cryptographically secure random secret using crypto/rand. This secret must be included in all requests:
- HTTP Requests:
X-Irgo-Secretheader - WebSocket Connections:
?secret=query parameter (WebSocket API limitation)
The secret is:
- 32 bytes of random data, base64 encoded (43 characters)
- Generated fresh for each application launch
- Injected into the WebView via JavaScript before page load
- Never logged or exposed in URLs (except WebSocket query param, which is unavoidable)
// Injected into WebView
window.__IRGO_SECRET__ = "randomSecretHere";This defeats DNS rebinding and CSRF attacks because external sites cannot know the secret.
Non-safe HTTP methods (POST, PUT, DELETE, PATCH) require a valid Origin header:
- Origin must exactly match the application's origin (e.g.,
http://127.0.0.1:PORT) - No suffix/prefix matching that could be bypassed
- Missing Origin is allowed for same-origin requests from the WebView
// Only exact matches allowed
if origin != allowedOrigin {
return 403 Forbidden
}CORS headers are set to only allow the application's own origin:
Access-Control-Allow-Origin: http://127.0.0.1:PORT
Access-Control-Allow-Credentials: true
The server only exposes routes defined by the application:
- No general RPC endpoint
- No file system access by default
- No shell execution by default
Middleware is applied in this order (outermost first):
// 1. CORS headers for preflight requests
handler = CORSMiddleware(allowedOrigins)(handler)
// 2. Origin validation for state-changing requests
handler = StrictOriginMiddleware(allowedOrigins)(handler)
// 3. Secret validation (excludes /static/)
handler = SecretValidationMiddleware(secret, []string{"/static/"})(handler)
// 4. WebSocket secret validation (in query param)
handler = WebSocketSecretMiddleware(secret)(handler)Static assets (/static/*) bypass secret validation for performance, but still require valid Origin for non-GET requests. This is safe because:
- GET requests to static assets cannot mutate state
- Static assets are typically cached by the browser
WebSocket connections face additional challenges:
- Browser WebSocket API does not support custom headers
- Secret must be passed as query parameter
The WebSocketSecretMiddleware validates the secret during the upgrade handshake before establishing the connection.
# Use in-process transport (no network server)
IRGO_TRANSPORT=inprocess ./myappconfig := desktop.Config{
Transport: "loopback", // Default - localhost HTTP server
// or
Transport: "inprocess", // No network - for testing mobile code path
}# Should fail (no secret)
curl http://127.0.0.1:PORT/api/data
# Should fail (wrong secret)
curl -H "X-Irgo-Secret: wrong" http://127.0.0.1:PORT/api/data# Should fail (wrong origin)
curl -X POST -H "Origin: http://evil.com" http://127.0.0.1:PORT/api/data-
WebSocket Secret in URL: The secret appears in the WebSocket URL query parameter. This is logged by some proxies/tools but is unavoidable due to browser API limitations.
-
Port Visibility: The application's port is visible to any local process. The secret prevents unauthorized access.
-
Memory-Based Attacks: A malicious process with memory access to the WebView could extract the secret. This is outside the threat model (assumes compromised local machine).
If you discover a security vulnerability, please report it privately by emailing [security contact] rather than opening a public issue.