Project: dashboard (Go + vanilla JS) Date: 2026-03-22 Auditor: security-scanner (automated)
| Severity | Count |
|---|---|
| CRITICAL | 1 |
| HIGH | 3 |
| MEDIUM | 4 |
| LOW | 3 |
File: internal/store/store.go:467-482
Severity: CRITICAL
The PostgresEventStore.psql() method passes SQL queries directly to the psql command-line tool via exec.CommandContext. While the Append() method uses quotePostgresLiteral() to escape single quotes in values, this escaping is insufficient to prevent all injection vectors:
quotePostgresLiteral()(line 505-507) only replaces'with''. It does not handle backslash escapes (\'), null bytes, or PostgreSQL dollar-quoting ($$...$$). Ifstandard_conforming_stringsisoffon the target server, a backslash-based escape can break out of the literal.- The
Recent()method (line 236) interpolates thelimitparameter directly into the SQL string viafmt.Sprintf. Whilelimitis anintand not directly user-controlled in the current code, this pattern is fragile and could become exploitable if the call chain changes. - The entire DSN (
s.cfg.DSN) is passed as a-dargument topsql(line 471). If the DSN is sourced from an untrusted environment variable, a crafted DSN could include shell metacharacters orpsqlconnection parameters that alter behavior (e.g.,sslrootcert,options).
Impact: If PostgreSQL integration is enabled and any event field (type, message, agentId, taskId) can be influenced by an external source, an attacker could execute arbitrary SQL on the database.
Recommendation: Replace the psql CLI shell-out with a proper Go database driver (lib/pq or pgx). If the CLI approach must be kept, use parameterized queries via psql variables or pipe input through stdin rather than embedding values in the command string.
File: internal/httpserver/server.go:115-144
Severity: HIGH
The handleWebSocket() method performs the WebSocket upgrade handshake without checking the Origin header. This allows any website to open a WebSocket connection to the dashboard server from a user's browser (Cross-Site WebSocket Hijacking / CSWSH).
An attacker-controlled page could:
- Read the full dashboard state (agents, tasks, events) in real time
- Send commands through the WebSocket (ping, pause_agent, resume_agent, reassign_task)
Impact: Information disclosure and unauthorized command execution via cross-origin WebSocket connections.
Recommendation: Validate the Origin header against an allowlist of expected origins before completing the upgrade handshake.
File: internal/httpserver/server.go:31-43
Severity: HIGH
The HTTP server does not set any security-related response headers. Missing headers include:
X-Content-Type-Options: nosniff— allows MIME-type sniffing attacksX-Frame-Options: DENY(orSAMEORIGIN) — allows clickjacking via iframe embeddingContent-Security-Policy— no CSP means no defense-in-depth against XSSStrict-Transport-Security— no HSTS if deployed behind TLSX-XSS-Protection— legacy but still useful for older browsersReferrer-Policy— no control over referrer leakage
Impact: The dashboard is vulnerable to clickjacking, MIME confusion, and lacks defense-in-depth against XSS.
Recommendation: Add a middleware that sets these headers on all responses.
File: internal/httpserver/server.go:33,37
Severity: HIGH
Two routes serve static files using http.FileServer(http.Dir(s.staticDir)):
mux.Handle("/app.js", http.FileServer(http.Dir(s.staticDir)))
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(s.staticDir))))Go's http.FileServer does sanitize paths and block .. traversal by default. However, the /static/ route exposes the entire staticDir directory tree. If staticDir is resolved to a parent directory (e.g., via symlinks or the ../../web fallback in resolveStaticDir()), files outside the intended web root could be served.
Additionally, resolveStaticDir() in cmd/server/main.go:92-119 walks up to ../../web from both cwd and the executable directory. If the binary is placed in an unexpected location, this could resolve to an unintended directory.
Impact: Potential exposure of files outside the intended web root depending on deployment layout.
Recommendation: Use filepath.Abs() + filepath.EvalSymlinks() on the resolved static directory and verify it is within the expected project boundary. Consider restricting /static/ to specific file extensions.
File: internal/httpserver/server.go:58-61
Severity: MEDIUM
The /api/state and /api/events endpoints do not set any CORS headers. While the absence of Access-Control-Allow-Origin means browsers will block cross-origin reads by default (which is good), there is no explicit CORS policy. If CORS headers are added later without care, the API could become open to cross-origin abuse.
The SSE endpoint (/api/events) is particularly notable because EventSource does not enforce same-origin by default in all browsers — it sends cookies cross-origin.
Impact: Without explicit CORS denial, future changes could inadvertently open the API to cross-origin access. SSE connections may leak state cross-origin if cookies are used for auth.
Recommendation: Explicitly set Access-Control-Allow-Origin to the expected origin (or omit it to rely on browser defaults) and add CORS preflight handling if the API will be accessed cross-origin.
File: internal/httpserver/server.go:300-338
Severity: MEDIUM
The readWebSocketFrame() function reads the frame length from the client and allocates a buffer of that size (line 328: payload := make([]byte, length)). A malicious client can send a frame header claiming a payload of several gigabytes, causing the server to attempt a massive memory allocation.
Impact: Denial of service via memory exhaustion from a single WebSocket connection.
Recommendation: Enforce a maximum frame size (e.g., 1 MB) and reject frames that exceed it.
File: cmd/server/main.go:32-34,62,74-75,87-88
Severity: MEDIUM
The server logs configuration details at startup including Redis endpoints, PostgreSQL DSN summaries, and NATS endpoints. While the summarize* functions attempt to strip credentials, the PostgreSQL DSN could still contain username/password in the URL path or query parameters that are not fully redacted by summarizePostgresDSN().
Additionally, store.go:534-581 (summarizePostgresDSN) preserves the scheme, host, and database name but does not explicitly strip user:password@ from URL-style DSNs.
Impact: Database credentials could be exposed in log output.
Recommendation: Ensure all summarize* functions explicitly strip userinfo from URLs before logging. Use parsed.Redacted() or manually clear parsed.User before formatting.
File: internal/httpserver/server.go (all routes)
Severity: MEDIUM
The entire HTTP server has no authentication or authorization mechanism. All endpoints (/api/state, /api/events, /ws, /static/) are publicly accessible. There is also no rate limiting, meaning:
- Any client can open unlimited SSE/WebSocket connections
- Any client can poll
/api/stateat arbitrary frequency - WebSocket commands (pause_agent, resume_agent, reassign_task) can be sent by anyone
Impact: Unauthorized access to dashboard state and command execution. Resource exhaustion via connection flooding.
Recommendation: Add authentication (at minimum, a shared secret or token) and rate limiting. For internal-only deployments, bind to 127.0.0.1 instead of 0.0.0.0.
File: cmd/server/main.go:35
Severity: LOW
The default bind address is :8080 (all interfaces). Combined with the lack of authentication (SEC-08), this exposes the dashboard to the entire network.
Impact: Network-adjacent attackers can access the dashboard.
Recommendation: Default to 127.0.0.1:8080 for local-only access. Require explicit configuration to bind to all interfaces.
File: cmd/server/main.go:35
Severity: LOW
The server uses http.ListenAndServe (plaintext HTTP). All data including dashboard state, events, and WebSocket commands are transmitted unencrypted.
Impact: Network eavesdropping can capture all dashboard data in transit.
Recommendation: Support http.ListenAndServeTLS or deploy behind a TLS-terminating reverse proxy.
File: web/app.js
Severity: LOW (Informational)
The frontend uses innerHTML in several places (renderTask, renderAgents, renderEvents, renderChips). All dynamic values are passed through escapeHtml() (line 640-647) which covers the five critical characters (&, <, >, ", ').
However, two locations set innerHTML with static strings that do not go through escapeHtml():
- Line 244:
elements.agentList.innerHTML = '暂无 agent 数据';(safe, static) - Line 46:
elements.taskCard.innerHTML = \...${escapeHtml(error.message)}...`` (properly escaped)
The escapeHtml function is comprehensive for HTML context injection. No XSS vulnerability was found in the current code. The textContent assignments in buildAgentItem and buildEventItem are inherently safe.
Note: The escapeHtml function does not protect against injection in URL or JavaScript contexts, but no such usage exists in the current code.
| File | Lines |
|---|---|
cmd/server/main.go |
128 |
internal/httpserver/server.go |
339 |
internal/provider/memory.go |
472 |
internal/provider/claude_runtime.go |
542 |
internal/store/store.go |
586 |
internal/bus/bus.go |
310 |
internal/config/config.go |
113 |
internal/model/model.go |
118 |
web/app.js |
648 |
web/index.html |
97 |