mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 21:31:09 +00:00
c7b297de83
- Deleted the deploy.cmd script as it was no longer needed. - Updated Dockerfile to include curl for health checks and added a non-root user for improved security. - Modified health check command to use curl for better reliability. - Adjusted docker-compose.test.yml to reflect changes in health check configuration. - Cleaned up appsettings.json and removed unused configuration properties. - Removed Resource entity and related requests from the codebase as part of the architectural shift. - Updated documentation to reflect the removal of hardware binding and related endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
147 lines
13 KiB
Markdown
147 lines
13 KiB
Markdown
# Static Analysis (SAST)
|
|
|
|
**Date**: 2026-05-13
|
|
**Method**: targeted code review of the cycle-1 surface (`/devices`, `/classes` CRUD, `/get-update`, `/resources/publish`) plus regression sweep over the pre-existing endpoints `/login`, `/users/*`, `/resources/*`. No automated SAST tool was run — all findings are manually identified with file/line evidence.
|
|
|
|
**Post-audit status (2026-05-13)**: F-1 closed via revert of AZ-183 (OTA feature deleted end-to-end); F-3 closed via UNIQUE INDEX `users_email_uidx` + `RegisterUser`/`RegisterDevice` consolidation; F-5 closed automatically as a consequence of F-1; D-1 closed via `Newtonsoft.Json` 13.0.4 bump. F-2 (path traversal) remains open — pre-existing, deferred to a separate ticket.
|
|
|
|
## Findings
|
|
|
|
### F-1: `/get-update` exposes per-resource plaintext `EncryptionKey` to any authenticated caller (HIGH — cycle-1 regression — **CLOSED via revert**)
|
|
|
|
- **Severity**: High
|
|
- **Status**: **CLOSED** (2026-05-13) — the entire OTA feature was deleted: `/get-update` and `/resources/publish` endpoints, `IResourceUpdateService` / `ResourceUpdateService` / `ResourceColumnEncryption`, the `Resource` entity, the `resources` table, the `apiUploaderPolicy` policy, the `ResourcesConfig.EncryptionMasterKey` config field, and the `e2e/Azaion.E2E/Tests/ResourceUpdateTests.cs` test class. AZ-183 is reverted; the OTA delivery model is itself obsolete in the target architecture (browser-only SaaS + fTPM-secured Jetsons).
|
|
- **Category**: Broken Access Control / Cryptographic Failures
|
|
- **Original locations** (now deleted):
|
|
- `Azaion.AdminApi/Program.cs:301-312` — endpoint registration used `.RequireAuthorization()` (any logged-in user).
|
|
- `Azaion.Services/ResourceUpdateService.cs:39-48` — every `ResourceUpdateItem` returned to the caller contained `EncryptionKey = ResourceColumnEncryption.Decrypt(resource.EncryptionKey, MasterKey)`.
|
|
- **Description**: The `resources.encryption_key` column was encrypted at rest with `ResourcesConfig.EncryptionMasterKey` precisely to mitigate DB compromise. But the application API decrypted and serialized that key into the HTTP response for any caller holding a valid JWT. A low-privilege user could submit `POST /get-update {Architecture, DevStage, CurrentVersions: {}}` and receive — for every published resource — `(CdnUrl, Sha256, EncryptionKey)`. With `EncryptionKey + CdnUrl`, the attacker could pull the encrypted blob from the CDN and decrypt it locally.
|
|
- **Impact (had it shipped)**: Confidentiality of every published resource (firmware, model weights, installer payloads) reduced to "any authenticated session".
|
|
- **Resolution rationale**: rather than tightening the policy or filtering the response, the user assessment is that the OTA delivery model itself is no longer needed — the suite is now installed/updated through the browser. Removing the surface eliminates the vulnerability and reduces the attack surface; F-5 (lazy-loaded `EncryptionMasterKey`) is also closed automatically.
|
|
|
|
### F-2: Path traversal via `dataFolder` route segment (HIGH — pre-existing, re-flagged)
|
|
|
|
- **Severity**: High
|
|
- **Category**: Broken Access Control / Injection (Path)
|
|
- **Locations**:
|
|
- `Azaion.Services/ResourcesService.cs:20-25` — `Path.Combine(ResourcesFolder, dataFolder)` accepts `..` and absolute paths without validation.
|
|
- Consumed by `Program.cs:201, 213, 219, 224` — `/resources/{dataFolder?}` (any-auth upload), `/resources/list/{dataFolder?}` (any-auth read), `/resources/clear/{dataFolder?}` (admin), `/resources/get/{dataFolder?}` (any-auth read).
|
|
- **Description**: `Path.Combine("Content", "../../etc")` resolves to `etc`, escaping the configured root. A non-admin caller can:
|
|
- List arbitrary directories via `/resources/list/../../`.
|
|
- Read encrypted contents of arbitrary files via `/resources/get/../../<path>` provided they know a filename.
|
|
- Write into arbitrary directories the process can write to via `/resources/<traversal>` upload.
|
|
- **Impact**: Server-side file disclosure and arbitrary file write bounded by process privileges. Pre-existing — already noted in `_docs/00_problem/security_approach.md` "Known Security Observations" #3 — but the cycle-1 audit re-confirms it is unmitigated.
|
|
- **Remediation**:
|
|
- Sanitize `dataFolder`: reject any value containing `..`, `/`, `\`, or starting with a drive letter; alternatively, allow only `[A-Za-z0-9_-]+` segments.
|
|
- Verify that the resolved absolute path starts with the resolved `ResourcesFolder` absolute path — `Path.GetFullPath(combined).StartsWith(Path.GetFullPath(root))` — and reject otherwise.
|
|
|
|
### F-3: `users.email` lacks a UNIQUE constraint — race in both `RegisterUser` and (cycle-1) `RegisterDevice` (HIGH — **CLOSED**)
|
|
|
|
- **Severity**: High
|
|
- **Status**: **CLOSED** (2026-05-13).
|
|
- **Resolution**:
|
|
- Added migration `env/db/06_users_email_unique.sql` containing `CREATE UNIQUE INDEX IF NOT EXISTS users_email_uidx ON public.users (email);`. The migration is wired into `e2e/db-init/00_run_all.sh`.
|
|
- `Azaion.Services/UserService.cs` — `RegisterUser` no longer check-then-inserts. It catches `Npgsql.PostgresException` with `SqlState == PostgresErrorCodes.UniqueViolation` (23505) and rethrows as `BusinessException(EmailExists)`. The race is closed atomically by the index.
|
|
- `RegisterDevice` was refactored to delegate the row insert to `RegisterUser` (the user's explicit guidance: "reuse the code in the implementation RegisterDevice -> should call RegisterUser"). Two concurrent provisioning calls that race on the same serial now hit the UNIQUE INDEX and surface `BusinessException(EmailExists)`; the caller can retry.
|
|
- **Residual risk**: a Postgres sequence for device serials (`device_serial_seq`) would also remove the serial-allocation race window and avoid the retry. Out of scope for this audit fix; can be added as a follow-up.
|
|
|
|
### F-4: `/devices` returns plaintext device password in the JSON body (MEDIUM — accepted by design, hardening required)
|
|
|
|
- **Severity**: Medium
|
|
- **Category**: Cryptographic Failures / Data Exposure
|
|
- **Locations**:
|
|
- `Azaion.AdminApi/Program.cs:158-162`
|
|
- `Azaion.Services/UserService.cs:84-89` (assembles `RegisterDeviceResponse`)
|
|
- **Description**: The endpoint deliberately returns the plaintext device password "exactly once" so the provisioning script can write it to `device.conf`. ApiAdmin-only, so abuse blast radius is bounded — but the password is reachable via:
|
|
- Reverse-proxy access logs that capture response bodies
|
|
- Browser DevTools / network history when triggered from the admin UI
|
|
- Swagger UI's "Try it out" response panel in any environment where Swagger is exposed (today: `IsDevelopment()` only — verified).
|
|
- **Impact**: Credentials may persist in unintended log sinks beyond their intended one-shot consumption.
|
|
- **Remediation**:
|
|
- Set response headers `Cache-Control: no-store, no-cache`, `Pragma: no-cache` on this endpoint specifically.
|
|
- Document an SRE runbook entry: do NOT enable response-body logging on the reverse proxy for `POST /devices`.
|
|
- Optional: add an `X-One-Shot-Credential: true` header so log scrubbers can match-and-mask.
|
|
|
|
### F-5: `EncryptionMasterKey` validation is lazy — first failing request, not startup (MEDIUM — **CLOSED**)
|
|
|
|
- **Severity**: Medium
|
|
- **Status**: **CLOSED** (2026-05-13) — `ResourcesConfig.EncryptionMasterKey` and the `ResourceUpdateService.MasterKey` getter were both deleted along with the OTA feature (see F-1). The lazy-validation surface no longer exists; `appsettings.json` and `docker-compose.test.yml` no longer reference the field.
|
|
|
|
### F-6: API container runs as root (MEDIUM)
|
|
|
|
- **Severity**: Medium
|
|
- **Category**: Security Misconfiguration
|
|
- **Location**: `Dockerfile:1, 20-25`
|
|
- **Description**: `mcr.microsoft.com/dotnet/aspnet:10.0` defaults to `root`. There is no `USER` directive after `FROM base AS final`. CIS Docker Benchmark §4.1 calls this out: a process running as root inside the container has more privileges than necessary, and a container escape (CVE-2024-21626 class) becomes a root-on-host exploit.
|
|
- **Impact**: Defense-in-depth weakness. No specific exploit, but failure mode is severe.
|
|
- **Remediation**: Add `USER app` to the final stage (the .NET 10 base image already provisions a non-root `app` user, UID 1654). The Content/log directory permissions need to be checked once the change is made.
|
|
|
|
### F-7: SHA-384 password hashing without per-user salt or KDF (MEDIUM — pre-existing)
|
|
|
|
- **Severity**: Medium
|
|
- **Category**: Cryptographic Failures
|
|
- **Locations**:
|
|
- `Azaion.Services/Security.cs:11-12` — `ToHash()` hashes raw UTF-8 bytes with SHA-384.
|
|
- `Azaion.Services/UserService.cs:44, 110, 78` — used for `RegisterUser`, `ValidateUser`, `RegisterDevice` (cycle-1 added the `RegisterDevice` call site).
|
|
- **Description**: SHA-384 is a fast cryptographic hash, not a password-hashing algorithm. No per-user salt, no work factor, no memory hardness. A leaked `password_hash` column lets an offline attacker grind ~10⁹ candidates per second per GPU.
|
|
- **Impact**: Database leak directly compromises all user passwords in tractable time.
|
|
- **Remediation**: Migrate to Argon2id (e.g., `Konscious.Security.Cryptography.Argon2`) or bcrypt (`BCrypt.Net-Next`) with per-user salt. Two-phase rollout: rehash on next successful login until the SHA-384 column is empty, then drop it.
|
|
|
|
### F-8: No rate limiting on `/login` (MEDIUM — pre-existing)
|
|
|
|
- **Severity**: Medium
|
|
- **Category**: Auth Failures
|
|
- **Location**: `Azaion.AdminApi/Program.cs:137-143`
|
|
- **Description**: Combined with F-7, an attacker who can reach `/login` can brute-force credentials. ASP.NET Core 10 ships `AddRateLimiter()` out of the box.
|
|
- **Remediation**: Add a fixed-window or sliding-window limiter scoped to `/login` (e.g., 10 requests / IP / minute, with exponential backoff).
|
|
|
|
### F-9: `LoginRequest`, `SetUserQueueOffsetsRequest` lack server-side validation (LOW — pre-existing)
|
|
|
|
- **Severity**: Low
|
|
- **Category**: Security Misconfiguration
|
|
- **Locations**:
|
|
- `Azaion.Common/Requests/LoginRequest.cs` — no validator class
|
|
- `Azaion.Common/Requests/SetUserQueueOffsetsRequest.cs` — no validator class
|
|
- **Description**: Other request DTOs use `AbstractValidator<T>` (`RegisterUserValidator`, `GetUpdateValidator`, etc.). These two are unguarded — `LoginRequest` accepts any-length email/password, `SetUserQueueOffsetsRequest` accepts any email shape and any offsets payload.
|
|
- **Remediation**: Add validators with `EmailAddress()` + `MinimumLength(12)` (matching `RegisterUserValidator`) and bounds checks for `Offsets`.
|
|
|
|
### F-10: Hardcoded credentials and JWT secret in test fixtures (LOW — accepted)
|
|
|
|
- **Severity**: Low
|
|
- **Category**: Hardcoded Credentials
|
|
- **Locations**:
|
|
- `docker-compose.test.yml:31-33, 37` — DB credentials, JWT secret, encryption master key as compose env vars.
|
|
- `e2e/Azaion.E2E/appsettings.test.json:4-7` — `AdminPassword`, `UploaderPassword`, `JwtSecret`.
|
|
- **Description**: These are e2e-only and consistent across the harness. They are NOT used in production builds. Flagged here for visibility only — they MUST NEVER drift into the prod compose / appsettings.
|
|
- **Remediation**: Add a CI guard: fail the pipeline if any of these literals appear in `Azaion.AdminApi/appsettings.json` or `Azaion.AdminApi/appsettings.Production.json`.
|
|
|
|
### F-11: `env/db/01_permissions.sql` ships placeholder DB passwords as a setup template (LOW)
|
|
|
|
- **Severity**: Low
|
|
- **Category**: Hardcoded Credentials (template / docs)
|
|
- **Location**: `env/db/01_permissions.sql:2, 7, 12` — `superadmin-pass`, `admin-pass`, `readonly-pass`.
|
|
- **Description**: The file is the operator setup template. The e2e harness immediately overrides these with `test_password` (`e2e/db-init/99_test_seed.sql:1-2`), so the placeholders never reach a runtime. But the file lives at `env/db/` with no header comment marking it template-only.
|
|
- **Remediation**: Add a top-of-file comment `-- TEMPLATE: replace placeholder passwords before applying to any environment.` Consider renaming to `01_permissions.example.sql`.
|
|
|
|
### F-12: Unstructured logging in `ResourcesService.SaveResource` (LOW)
|
|
|
|
- **Severity**: Low
|
|
- **Category**: Logging Failures (operational)
|
|
- **Location**: `Azaion.Services/ResourcesService.cs:63` — `logger.LogInformation($"Resource {data.FileName} Saved Successfully")`.
|
|
- **Description**: String interpolation defeats Serilog's structured property capture; the `FileName` is not searchable as a field. Not a security issue, but flagged because the security-event-logging principle (audit trail) requires structured fields.
|
|
- **Remediation**: `logger.LogInformation("Resource {FileName} saved successfully", data.FileName);`
|
|
|
|
### F-13: No HTTPS enforcement in application code (LOW — pre-existing, design)
|
|
|
|
- **Severity**: Low
|
|
- **Category**: Cryptographic Failures
|
|
- **Location**: `Azaion.AdminApi/Program.cs` — no `app.UseHttpsRedirection()`, no `Hsts`.
|
|
- **Description**: HTTPS is assumed at the reverse proxy. Acceptable design choice if and only if the reverse proxy and its config are part of the secure boundary.
|
|
- **Remediation**: Document the assumption in a deployment runbook; consider `UseHsts()` when the upstream chain terminates TLS.
|
|
|
|
## Self-verification
|
|
|
|
- [x] All source directories scanned (`Azaion.AdminApi/`, `Azaion.Services/`, `Azaion.Common/`, `env/db/`, `Dockerfile`)
|
|
- [x] Each finding has file path and (where relevant) line numbers
|
|
- [x] No false positives from test files or comments — test-fixture credentials (F-10) are explicitly framed as accepted-risk
|