docs/qe-reports/02-security-review.md
Date: 2026-04-05 Reviewer: QE Security Reviewer (V3) Scope: Full codebase -- Python API, Rust crates, ESP32 C firmware Severity Weights: CRITICAL=3, HIGH=2, MEDIUM=1, LOW=0.5, INFORMATIONAL=0.25 Weighted Finding Score: 19.25 (minimum required: 3.0)
This security review examined all security-sensitive code across the wifi-densepose project: the Python FastAPI backend (authentication, rate limiting, CORS, WebSocket, API endpoints), Rust workspace crates (API, DB, config, WASM), and ESP32-S3 C firmware (NVS credentials, OTA update, WASM upload, swarm bridge, UDP streaming).
Recommendation: CONDITIONAL PASS -- No critical data-exfiltration or remote code execution vulnerabilities were found in the production code paths. However, 3 HIGH severity findings and several MEDIUM issues require remediation before any production deployment. The codebase demonstrates solid security awareness in many areas (constant-time OTA PSK comparison, Ed25519 WASM signature verification, parameterized queries via SQLAlchemy/sqlx, bcrypt password hashing), but gaps remain in WebSocket security, rate limiting bypass vectors, and firmware transport encryption.
| Severity | Count | Categories |
|---|---|---|
| CRITICAL | 0 | -- |
| HIGH | 3 | Auth bypass, information disclosure, IP spoofing |
| MEDIUM | 7 | CORS, token lifecycle, transport security, memory growth |
| LOW | 5 | Deprecated APIs, logging, configuration hardening |
| INFORMATIONAL | 3 | Best practice improvements |
Severity: HIGH OWASP: A07:2021 -- Identification and Authentication Failures Files:
archive/v1/src/api/routers/stream.py:74 (WebSocket token query parameter)archive/v1/src/middleware/auth.py:243 (fallback to request.query_params.get("token"))archive/v1/src/api/middleware/auth.py:173 (request.query_params.get("token"))Description: JWT tokens are accepted via URL query parameters for WebSocket connections. URL parameters are logged in web server access logs, browser history, proxy logs, and HTTP Referer headers. This creates multiple credential leakage vectors.
# archive/v1/src/api/routers/stream.py:74
token: Optional[str] = Query(None, description="Authentication token")
# archive/v1/src/middleware/auth.py:243
if request.url.path.startswith("/ws"):
token = request.query_params.get("token")
Impact: JWT tokens may be captured from server logs, proxy caches, or browser history, enabling session hijacking.
Remediation:
Sec-WebSocket-Protocol header to pass tokens during the upgrade handshake.token parameter.Severity: HIGH
OWASP: A05:2021 -- Security Misconfiguration
File: archive/v1/src/middleware/rate_limit.py:200-206
Description:
The _get_client_ip method trusts the X-Forwarded-For header without any validation. An attacker can spoof this header to bypass IP-based rate limiting entirely by rotating forged IP addresses on each request.
# archive/v1/src/middleware/rate_limit.py:200-206
def _get_client_ip(self, request: Request) -> str:
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
return forwarded_for.split(",")[0].strip()
real_ip = request.headers.get("X-Real-IP")
if real_ip:
return real_ip
return request.client.host if request.client else "unknown"
Impact: Complete rate limiting bypass for unauthenticated requests. An attacker can send unlimited requests by setting arbitrary X-Forwarded-For values.
Remediation:
X-Forwarded-For when the application is deployed behind a known reverse proxy. Configure a trusted proxy allowlist.--proxy-headers flag only when behind a trusted proxy, and strip these headers at the edge.starlette.middleware.trustedhost.TrustedHostMiddleware and validating the number of proxy hops.Severity: HIGH OWASP: A09:2021 -- Security Logging and Monitoring Failures Files:
archive/v1/src/api/routers/pose.py:140-141 -- detail=f"Pose estimation failed: {str(e)}"archive/v1/src/api/routers/pose.py:176-177 -- detail=f"Pose analysis failed: {str(e)}"archive/v1/src/api/routers/stream.py:297 -- detail=f"Failed to get stream status: {str(e)}"archive/v1/src/api/routers/stream.py (lines 326, 351, 404, 442, 463)archive/v1/src/middleware/error_handler.py:101-104 -- traceback in development modeDescription:
Multiple API endpoints directly interpolate Python exception messages into HTTP error responses. While the global error handler in error_handler.py correctly suppresses details in production, the per-endpoint HTTPException handlers bypass this and always expose str(e) regardless of environment.
# archive/v1/src/api/routers/pose.py:140-141
raise HTTPException(
status_code=500,
detail=f"Pose estimation failed: {str(e)}"
)
Impact: Internal error messages (including database connection strings, file paths, stack traces, and library-specific error codes) are exposed to unauthenticated callers. This aids reconnaissance for targeted attacks.
Remediation:
detail=f"...{str(e)}" patterns with a generic message: detail="Internal server error".logger.exception().ErrorHandler class for all error formatting, which already has production-safe behavior.Severity: MEDIUM OWASP: A05:2021 -- Security Misconfiguration Files:
archive/v1/src/config/settings.py:33-34 -- defaults: cors_origins=["*"], cors_allow_credentials=Truearchive/v1/src/middleware/cors.py:255-256 -- development config combines allow_origins=["*"] + allow_credentials=TrueDescription:
The default settings allow CORS from all origins (*) with credentials (allow_credentials=True). Per the CORS specification, Access-Control-Allow-Origin: * cannot be used with Access-Control-Allow-Credentials: true. However, the CORSMiddleware implementation echoes the requesting origin header verbatim, effectively granting credentialed access from any origin.
# archive/v1/src/middleware/cors.py:255-256 (development_config)
"allow_origins": ["*"],
"allow_credentials": True,
The validate_cors_config function at line 354 correctly flags this combination but is only advisory -- it does not prevent the configuration from being applied.
Impact: Any website can make authenticated cross-origin requests to the API when running in development mode. If development defaults leak to production, this becomes a credential theft vector via CSRF-like attacks.
Remediation:
cors_origins to [] (empty list) and require explicit configuration.validate_cors_config enforce the rule by raising an exception rather than returning warnings.CORSMiddleware.__init__, reject the combination of allow_credentials=True with wildcard origins at construction time.Severity: MEDIUM OWASP: A04:2021 -- Insecure Design Files:
archive/v1/src/api/routers/stream.py:127-128 -- message = await websocket.receive_text() with no size limitarchive/v1/src/api/websocket/connection_manager.py -- no max_size configurationDescription:
WebSocket endpoints accept incoming messages of arbitrary size. The receive_text() call at stream.py:127 has no size limit, allowing a client to send extremely large messages that consume server memory.
Additionally, the ConnectionManager does not enforce a maximum number of connections. An attacker could open thousands of WebSocket connections to exhaust server resources.
Impact: Denial of service through memory exhaustion or connection pool exhaustion.
Remediation:
websocket.accept(max_size=...) or use Starlette's WebSocket max_size parameter (default is 16 MB -- reduce to 64 KB or less for control messages).ConnectionManager.connect() and reject new connections when the limit is reached.Severity: MEDIUM
OWASP: A07:2021 -- Identification and Authentication Failures
File: archive/v1/src/api/middleware/auth.py:246-252
Description:
The TokenBlacklist class clears all blacklisted tokens every hour, regardless of their actual expiry time. This means:
# archive/v1/src/api/middleware/auth.py:246-252
def _cleanup_if_needed(self):
now = datetime.utcnow()
if (now - self._last_cleanup).total_seconds() > self._cleanup_interval:
self._blacklisted_tokens.clear() # Clears ALL tokens
self._last_cleanup = now
Furthermore, the TokenBlacklist is not consulted in the AuthMiddleware.dispatch() or AuthenticationMiddleware._authenticate_request() flows -- the token_blacklist global instance exists but is never checked during token validation.
Impact: Token revocation (logout) is not enforceable. A stolen JWT remains valid until its natural expiry.
Remediation:
exp claim timestamp. Only remove entries whose exp has passed._verify_token() / verify_token() so that blacklisted tokens are rejected.Severity: MEDIUM
OWASP: A07:2021 -- Identification and Authentication Failures
File: firmware/esp32-csi-node/main/ota_update.c:44-49
Description:
The OTA firmware update endpoint (POST /ota on port 8032) has authentication disabled unless an OTA pre-shared key (PSK) is manually provisioned into NVS. The ota_check_auth function returns true when no PSK is configured, allowing unauthenticated firmware uploads.
// firmware/esp32-csi-node/main/ota_update.c:44-49
static bool ota_check_auth(httpd_req_t *req)
{
if (s_ota_psk[0] == '\0') {
/* No PSK provisioned -- auth disabled (permissive for dev). */
return true;
}
...
}
The firmware logs a warning about this (ESP_LOGW(..., "OTA authentication DISABLED")), but it is the default state for all new devices.
Impact: Any device on the same network can flash arbitrary firmware to the ESP32 without authentication, enabling persistent compromise of the sensing node.
Remediation:
Severity: MEDIUM
OWASP: A02:2021 -- Cryptographic Failures
File: firmware/esp32-csi-node/main/stream_sender.c:66-106
Description:
CSI data frames are transmitted via plain UDP (SOCK_DGRAM, IPPROTO_UDP) with no encryption, authentication, or integrity protection. An attacker on the same network segment can:
// firmware/esp32-csi-node/main/stream_sender.c:92-93
int sent = sendto(s_sock, data, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
Impact: CSI data exposure and injection on the local network. The severity is moderated by the fact that CSI data requires specialized knowledge to interpret, but the UDP transport provides zero confidentiality for the sensor data.
Remediation:
Severity: MEDIUM
OWASP: A02:2021 -- Cryptographic Failures
File: firmware/esp32-csi-node/main/swarm_bridge.c:211-229
Description:
The swarm bridge HTTP client configuration does not enforce TLS. The esp_http_client_config_t struct at line 211 specifies only .url and .timeout_ms without setting .transport_type = HTTP_TRANSPORT_OVER_SSL or .cert_pem. If the seed_url uses http:// rather than https://, the Bearer token is transmitted in cleartext.
// firmware/esp32-csi-node/main/swarm_bridge.c:211-216
esp_http_client_config_t http_cfg = {
.url = url,
.method = HTTP_METHOD_POST,
.timeout_ms = SWARM_HTTP_TIMEOUT,
};
// firmware/esp32-csi-node/main/swarm_bridge.c:226-229
if (s_cfg.seed_token[0] != '\0') {
char auth_hdr[80];
snprintf(auth_hdr, sizeof(auth_hdr), "Bearer %s", s_cfg.seed_token);
esp_http_client_set_header(client, "Authorization", auth_hdr);
}
Impact: Bearer token can be sniffed on the local network, enabling unauthorized access to the Cognitum Seed ingest API.
Remediation:
seed_url starts with https:// in swarm_bridge_init() and reject http:// URLs.Severity: MEDIUM OWASP: A04:2021 -- Insecure Design Files:
archive/v1/src/api/middleware/rate_limit.py:28-29 -- self.request_counts = defaultdict(lambda: deque())archive/v1/src/middleware/rate_limit.py:132 -- self._sliding_windows: Dict[str, SlidingWindowCounter] = {}Description:
Both rate limiter implementations store per-client sliding window data in unbounded in-memory dictionaries. An attacker sending requests from many spoofed IPs (see HIGH-002) can create millions of entries, each containing a deque of timestamps. The cleanup tasks run only periodically (every 5 minutes or on-demand) and cannot keep pace with a high-rate attack.
Impact: Memory exhaustion denial of service through rate limiter state amplification.
Remediation:
Severity: LOW
OWASP: A07:2021 -- Identification and Authentication Failures
File: v1/test_auth_rate_limit.py:26
Description: A test script in the repository contains a hardcoded JWT secret key placeholder:
SECRET_KEY = "your-secret-key-here" # This should match your settings
While marked with a comment indicating it should be changed, this file is checked into the repository and could be mistaken for a real configuration.
Impact: Low -- this is a test file, not production configuration. However, if a developer copies this value into production settings, JWT tokens become trivially forgeable.
Remediation:
SECRET_KEY = os.environ.get("SECRET_KEY", "").Severity: LOW OWASP: A01:2021 -- Broken Access Control Files:
archive/v1/src/middleware/auth.py:298-299 -- response.headers["X-User"] = user_info["username"] and response.headers["X-User-Roles"] = ",".join(user_info["roles"])archive/v1/src/api/middleware/auth.py:111 -- response.headers["X-User-ID"] = request.state.user.get("id", "")Description: Authenticated user information (username, roles, user ID) is included in HTTP response headers. These headers are visible to any intermediary (CDN, reverse proxy, browser extensions) and in browser developer tools.
Impact: Information disclosure of user identity and authorization roles to intermediaries and client-side code.
Remediation:
X-User, X-User-Roles, and X-User-ID response headers, or restrict them to internal/debug environments only.datetime.utcnow() Usage (CWE-1235)Severity: LOW Files: Throughout the Python codebase (auth.py, rate_limit.py, connection_manager.py, pose_stream.py, error_handler.py, stream.py)
Description:
datetime.utcnow() is deprecated in Python 3.12+ in favor of datetime.now(datetime.timezone.utc). While not a security vulnerability per se, timezone-naive datetimes can cause token expiry comparison bugs in environments where the system clock timezone differs from UTC.
Remediation:
Replace all instances of datetime.utcnow() with datetime.now(datetime.timezone.utc).
Severity: LOW
OWASP: A02:2021 -- Cryptographic Failures
File: archive/v1/src/config/settings.py:30 -- jwt_algorithm: str = Field(default="HS256")
Description: The default JWT algorithm is HS256 (HMAC-SHA256), a symmetric algorithm. This means the same secret is used for both signing and verification, requiring the secret to be distributed to every service that needs to verify tokens. For multi-service architectures, asymmetric algorithms (RS256, ES256) are preferred.
Additionally, the jwt_algorithm setting is not validated against a safe algorithm allowlist, leaving open the possibility of configuration to none (no signature).
Remediation:
jwt_algorithm against an allowlist of safe algorithms: ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"].none algorithm.Severity: LOW
OWASP: A07:2021 -- Identification and Authentication Failures
File: archive/v1/src/middleware/auth.py:115 -- create_user() method
Description:
The create_user() method accepts any password without minimum length, complexity, or entropy requirements. Test credentials in v1/test_auth_rate_limit.py:21-23 demonstrate weak passwords ("admin123", "user123").
Remediation:
Files:
v2/crates/wifi-densepose-api/src/lib.rs -- //! WiFi-DensePose REST API (stub)v2/crates/wifi-densepose-db/src/lib.rs -- //! WiFi-DensePose database layer (stub)v2/crates/wifi-densepose-config/src/lib.rs -- //! WiFi-DensePose configuration (stub)Description:
The Rust API, database, and configuration crates contain only single-line stub comments. No security review of Rust API endpoints, database queries, or configuration handling was possible because no implementation exists. The wifi-densepose-sensing-server crate contains the actual Rust server implementation.
Note: The sensing server (crates/wifi-densepose-sensing-server/src/main.rs) was checked for SQL injection patterns, CORS issues, and authentication concerns. No SQL injection risks were found (no string-formatted queries). The server appears to use in-memory data structures rather than a database.
unsafe Blocks in WASM Edge CrateFiles: v2/crates/wifi-densepose-wasm-edge/src/*.rs (multiple files)
Description:
The wifi-densepose-wasm-edge crate contains approximately 40 unsafe blocks, primarily for:
static mut EVENTS: [...])repr(C) struct serialization in rvf.rsThese patterns are common in no_std WASM edge environments where heap allocation is unavailable. The static event arrays use a fixed-size pattern (EVENTS[..n]) that prevents out-of-bounds writes as long as n is bounded correctly. Visual inspection of the bounds checks suggests they are correct, but formal verification or fuzzing of the bounds logic is recommended.
The main workspace crate (wifi-densepose-train) explicitly notes it avoids unsafe blocks.
Files: firmware/esp32-csi-node/main/*.c
Description:
The firmware codebase consistently uses strncpy with explicit null termination, snprintf (not sprintf), and proper bounds checking throughout. No instances of strcpy, strcat, sprintf, or gets were found. Buffer sizes are defined via #define constants. The rvf_parser.c performs thorough size validation before any pointer arithmetic.
This is a positive finding reflecting good security practices.
requirements.txt)| Package | Version Spec | Risk |
|---|---|---|
python-jose[cryptography]>=3.3.0 | MEDIUM -- python-jose has had JWT confusion vulnerabilities. Consider migrating to PyJWT or authlib. | |
paramiko>=3.0.0 | LOW -- SSH library. Ensure latest minor version for CVE patches. | |
fastapi>=0.95.0 | LOW -- Version floor is old. Pin to latest stable for security patches. |
Recommendation: Run pip audit or safety check against the locked dependency file (archive/v1/requirements-lock.txt) to identify known CVEs.
Cargo.toml)| Crate | Version | Notes |
|---|---|---|
sqlx 0.7 | OK -- uses parameterized queries by design. | |
axum 0.7 | OK -- current major version. | |
wasm-bindgen 0.2 | OK -- standard WASM interface. |
Recommendation: Run cargo audit against Cargo.lock to check for known advisories.
The following areas demonstrate security-conscious design:
OTA PSK constant-time comparison (firmware/esp32-csi-node/main/ota_update.c:66-72): Uses XOR-accumulator pattern to prevent timing attacks on authentication.
WASM signature verification (firmware/esp32-csi-node/main/wasm_upload.c:112-137): Ed25519 signature verification is enabled by default (wasm_verify=1). Unsigned uploads are rejected unless explicitly disabled via Kconfig.
RVF build hash validation (firmware/esp32-csi-node/main/rvf_parser.c:126-137): SHA-256 hash of the WASM payload is verified against the manifest before loading, preventing tampered module execution.
Password hashing with bcrypt (archive/v1/src/middleware/auth.py:21): Proper use of passlib with bcrypt scheme.
Protected user fields (archive/v1/src/middleware/auth.py:139): update_user() prevents modification of username, created_at, and hashed_password.
Production error suppression (archive/v1/src/middleware/error_handler.py:214-218): The centralized error handler correctly suppresses internal details in production mode.
No hardcoded secrets in source (verified via entropy-based search across entire repository): No API keys, passwords, or tokens found in source files (the test script placeholder at test_auth_rate_limit.py:26 is marked as requiring replacement).
.env file excluded via .gitignore (.gitignore:171): Environment files are properly excluded from version control.
C string safety (all firmware/esp32-csi-node/main/*.c): Consistent use of strncpy, snprintf, and null-termination guards. No unsafe C string functions.
NVS input validation (firmware/esp32-csi-node/main/nvs_config.c): Bounds checking on all NVS-loaded values (channel range, dwell time minimums, array index clamping).
archive/v1/src/middleware/auth.py (457 lines) -- JWT auth, user management, middlewarearchive/v1/src/middleware/rate_limit.py (465 lines) -- Rate limiting with sliding windowarchive/v1/src/middleware/cors.py (375 lines) -- CORS middleware and validationarchive/v1/src/middleware/error_handler.py (505 lines) -- Error handling middlewarearchive/v1/src/api/middleware/auth.py (303 lines) -- API-layer JWT autharchive/v1/src/api/middleware/rate_limit.py (326 lines) -- API-layer rate limitingarchive/v1/src/api/websocket/connection_manager.py (461 lines) -- WebSocket managerarchive/v1/src/api/websocket/pose_stream.py (384 lines) -- Pose streaming handlerarchive/v1/src/api/routers/pose.py (420 lines) -- Pose API endpointsarchive/v1/src/api/routers/stream.py (465 lines) -- Streaming API endpointsarchive/v1/src/config/settings.py (436 lines) -- Application settingsarchive/v1/src/sensing/rssi_collector.py (partial) -- Subprocess usage reviewarchive/v1/src/tasks/backup.py (partial) -- Subprocess command constructionv1/test_auth_rate_limit.py (partial) -- Test credentials reviewcrates/wifi-densepose-api/src/lib.rs (1 line -- stub)crates/wifi-densepose-db/src/lib.rs (1 line -- stub)crates/wifi-densepose-config/src/lib.rs (1 line -- stub)crates/wifi-densepose-wasm/src/lib.rs (133 lines) -- WASM bindingscrates/wifi-densepose-wasm/src/mat.rs (partial) -- MAT dashboardcrates/wifi-densepose-wasm-edge/src/*.rs (unsafe block audit)crates/wifi-densepose-sensing-server/src/main.rs (SQL injection pattern search)Cargo.toml (workspace dependencies)main.c (302 lines) -- Application entry pointnvs_config.c (333 lines) -- NVS configuration loadingnvs_config.h (77 lines) -- Configuration struct definitionsstream_sender.c (117 lines) -- UDP stream senderota_update.c (267 lines) -- OTA firmware updatewasm_upload.c (433 lines) -- WASM module managementrvf_parser.c (169+ lines) -- RVF container parserswarm_bridge.c (328 lines) -- Cognitum Seed bridgerequirements.txt (47 lines).gitignore (verified .env exclusion)| Check Category | Patterns Searched | Result |
|---|---|---|
| Hardcoded secrets | password=, secret_key=, api_key=, high-entropy strings | Clean (1 test placeholder found) |
| SQL injection | String-formatted SQL queries (format! + SQL keywords, f-string + SQL) | Clean |
| Command injection | subprocess with user input, os.system, eval | Safe (fixed command arrays only) |
| Path traversal | User-controlled file paths without sanitization | Not applicable (no file serving endpoints) |
| Insecure deserialization | pickle.loads, yaml.unsafe_load, eval on user input | Clean |
| Weak cryptography | md5, sha1 for security, DES, RC4 | Clean (uses bcrypt, SHA-256, Ed25519) |
| Unsafe C functions | strcpy, strcat, sprintf, gets | Clean (uses safe alternatives throughout) |
| Unsafe Rust blocks | unsafe { ... } in workspace crates | ~40 in wasm-edge (acceptable for no_std) |
.env files committed | .env, .env.local, .env.production | Clean (properly gitignored) |
| CORS misconfiguration | Wildcard + credentials | Found (MEDIUM-001) |
| Priority | Finding | Effort | Impact |
|---|---|---|---|
| 1 | HIGH-002: Rate limiter IP spoofing | Low | Eliminates rate limiting bypass |
| 2 | HIGH-001: WebSocket token in URL | Medium | Prevents credential leakage |
| 3 | HIGH-003: Error detail exposure | Low | Prevents information disclosure |
| 4 | MEDIUM-003: Token blacklist not enforced | Medium | Enables logout functionality |
| 5 | MEDIUM-004: OTA default no-auth | Low | Prevents unauthorized firmware flash |
| 6 | MEDIUM-002: WebSocket message limits | Low | Prevents DoS via large messages |
| 7 | MEDIUM-001: CORS wildcard + credentials | Low | Prevents CSRF-like attacks |
| 8 | MEDIUM-005: UDP stream no encryption | High | Adds transport security |
| 9 | MEDIUM-006: Swarm bridge cleartext | Medium | Protects Seed authentication |
| 10 | MEDIUM-007: Rate limiter memory growth | Medium | Prevents state amplification DoS |
| Category | Score | Max | Notes |
|---|---|---|---|
| Authentication | 6/10 | 10 | Good JWT implementation; token blacklist non-functional |
| Authorization | 8/10 | 10 | Role-based access control present; missing RBAC on some endpoints |
| Input Validation | 8/10 | 10 | Pydantic models, NVS bounds checks; WebSocket lacks size limits |
| Cryptography | 7/10 | 10 | bcrypt, Ed25519, SHA-256; UDP transport unencrypted |
| Configuration | 6/10 | 10 | Good validation functions; unsafe defaults for development |
| Error Handling | 7/10 | 10 | Centralized handler good; per-endpoint leaks |
| Transport Security | 5/10 | 10 | No TLS enforcement for firmware; no DTLS for UDP |
| Dependency Security | 7/10 | 10 | Reasonable version floors; no pinned versions |
| Firmware Security | 7/10 | 10 | OTA auth optional; WASM verification strong |
| Logging/Monitoring | 7/10 | 10 | Comprehensive logging; token blacklist not wired |
Overall Security Score: 68/100
Generated by QE Security Reviewer (V3) -- Domain: security-compliance (ADR-008)