7.5 KiB
Code Audit Report — httpserver stack
Date: 2026-03-05
Scope: /opt/stacks/httpserver (compose, data/, chat server, services loader, static assets)
Executive summary
- Security: Several issues: sensitive file exposure, client-side XSS risk in chat, missing escaping in services HTML, no chat rate limiting or abuse controls.
- Correctness: One protocol mismatch (gs.html chat history never populates).
- Maintainability: Minor issues (comment vs script name, dependency pinning).
1. Security
1.1 Sensitive file served as static content (High)
File: data/banned.json
Issue: The entire data/ directory is mounted as the Apache document root (./data:/var/www/html). So banned.json is publicly reachable at /banned.json. It contains fail2ban-style jail names and IP addresses.
Impact: Information disclosure (internal IPs, jail names). Useful for reconnaissance.
Recommendation:
- Move
banned.jsonoutside the web root (e.g. project root or a non-served volume) if it must live in the repo, or - Exclude it from the volume used as document root (e.g. serve only a whitelisted subdirectory), or
- Stop serving it over HTTP (e.g. use it only in a backend/script that doesn’t expose it).
1.2 Chat client uses innerHTML for message text (Medium)
File: data/chat.js (around line 102)
// Comment says "use textContent for safety" but code uses innerHTML:
textSpan.innerHTML = msg.text;
Issue: The server sanitizes only < and > in chat-server.js. Using innerHTML with server output is fragile: if sanitization is ever relaxed or bypassed, or if the client receives data from another path, this becomes an XSS sink.
Recommendation: Use textContent for the message body so the DOM is not parsed as HTML. If you need newlines, use textContent and style with white-space: pre-wrap (or similar).
1.3 Services HTML built from JSON without escaping (Medium)
File: data/services-loader.js
Issue: Service name, url, category, and meta[].label / meta[].url are interpolated into HTML strings (e.g. href="${meta.url}", ${service.name}) without encoding. If services-data.json is ever edited incorrectly, compromised, or merged with untrusted data, a value containing " or > could break attributes or inject script.
Recommendation: Add a small escapeHtml (and optionally escapeAttr) helper and use it for every value that is inserted into HTML or attributes. Keep treating services-data.json as trusted input, but defense-in-depth avoids mistakes and future data sources.
1.4 Chat server: no rate limiting or abuse controls (Medium)
File: data/chat-server.js
Issue: Any client can connect and send unlimited messages. There is no per-IP or per-connection rate limit, no use of banned.json, and no max message size beyond the 200-character sanitizer slice.
Impact: DoS via message flood; spam; possible memory pressure from a very large history if HISTORY_MAX is raised.
Recommendation: Add rate limiting (e.g. per connection: max N messages per minute). Optionally enforce max message size and consider using banned.json (or a similar list) to reject connections from banned IPs if the proxy passes client IP (e.g. via X-Forwarded-For and a trusted proxy config).
1.5 Chat sanitization is minimal (Low)
File: data/chat-server.js — sanitize()
Issue: Only < and > are escaped. For plain text in a <div> (and if the client uses textContent as recommended), this is enough for basic XSS prevention. It does not normalize Unicode or protect against other edge cases (e.g. if the same string were ever used in an attribute).
Recommendation: Keep server-side sanitization and switch the client to textContent. If you later use the same string in attributes or other contexts, add encoding appropriate to that context (e.g. attribute encoding).
2. Correctness
2.1 Chat history protocol mismatch in gs.html (Bug)
Files: data/chat-server.js vs data/gs.html
Issue: The server sends history as:
{ type: 'history', messages: history }
gs.html expects:
if (Array.isArray(msg.history)) { ... }
So msg.history is always undefined and chat history is never shown on the GS page.
Recommendation: In gs.html, use msg.messages when data.type === 'history' (and optionally check Array.isArray(msg.messages)), consistent with chat.js.
3. Configuration & deployment
3.1 Apache proxy config duplication
File: data/chat-proxy.conf
Issue: Both RewriteRule and ProxyPass/ProxyPassReverse are used for /chat → WebSocket. This can be redundant or confusing; one consistent mechanism (e.g. mod_rewrite with [P] or ProxyPass) is easier to reason about.
Recommendation: Prefer one approach (e.g. RewriteRule with [P,L] for WebSocket upgrade, and ensure no double proxy). Document which Apache modules are required.
3.2 Chat server port exposure
File: compose.yaml
Issue: The chat service publishes 8098:8081. So the WebSocket server is reachable on host port 8098 without going through Apache. If Apache is the intended single entry point for the site, consider not publishing the chat port on the host (only expose it on internal-net so Apache can proxy to it).
Recommendation: Remove the ports: mapping for the chat service if all access should be via Apache’s /chat; otherwise document that 8098 is intentionally public.
3.3 Dependency pinning
File: data/package.json
Issue: "ws": "^8.18.0" allows minor/patch updates. Rebuilds can pull different versions.
Recommendation: Use exact versions (e.g. "ws": "8.18.0") or lock with package-lock.json committed and npm ci in the image for reproducible builds.
4. Maintainability
4.1 Outdated comment / missing script
File: data/services-loader.js (header comment)
Issue: Historical note: the loader previously had an embedded-data workflow and referenced a script for syncing/embedding.
Recommendation: Keep services-data.json as the single source of truth and load it at runtime (no embed/sync step).
5. Positive notes
- Chat server sanitizes nickname and message length and strips
</>. gs.htmluses anescapeHtmlhelper for chat nick and text when building HTML.main.jsrespectsprefers-reduced-motionand limits star count on small viewports.- Compose resource limits and logging options are set; networks are isolated.
.envin project root is not under the web root, so it is not served.
6. Summary of recommended actions (all applied)
| Priority | Item | Status |
|---|---|---|
| High | Stop serving banned.json |
Done: Apache <Files "banned.json"> Require all denied in chat-proxy.conf |
| Medium | In chat.js, use textContent for message text |
Done |
| Medium | In services-loader.js, escape all dynamic values |
Done: escapeHtml / escapeAttr added and used |
| Medium | Add rate limiting to chat server | Done: 30 msg/min per connection (configurable via CHAT_RATE_LIMIT) |
| Medium | Fix gs.html to use msg.messages for history |
Done |
| Low | Simplify Apache proxy config and document | Done: comment + deny for banned.json |
| Low | Do not publish chat port; use Apache only | Done: port removed; gs.html uses location.host + '/chat' |
| Low | Pin ws; fix services-loader.js comment |
Done: "ws": "8.18.0"; loader now reads services-data.json directly (no status/embed script). |