mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 06:51:08 +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>
13 KiB
13 KiB
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-updateand/resources/publishendpoints,IResourceUpdateService/ResourceUpdateService/ResourceColumnEncryption, theResourceentity, theresourcestable, theapiUploaderPolicypolicy, theResourcesConfig.EncryptionMasterKeyconfig field, and thee2e/Azaion.E2E/Tests/ResourceUpdateTests.cstest 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— everyResourceUpdateItemreturned to the caller containedEncryptionKey = ResourceColumnEncryption.Decrypt(resource.EncryptionKey, MasterKey).
- Description: The
resources.encryption_keycolumn was encrypted at rest withResourcesConfig.EncryptionMasterKeyprecisely 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 submitPOST /get-update {Architecture, DevStage, CurrentVersions: {}}and receive — for every published resource —(CdnUrl, Sha256, EncryptionKey). WithEncryptionKey + 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 toetc, 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.
- List arbitrary directories via
- 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
ResourcesFolderabsolute path —Path.GetFullPath(combined).StartsWith(Path.GetFullPath(root))— and reject otherwise.
- Sanitize
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.sqlcontainingCREATE UNIQUE INDEX IF NOT EXISTS users_email_uidx ON public.users (email);. The migration is wired intoe2e/db-init/00_run_all.sh. Azaion.Services/UserService.cs—RegisterUserno longer check-then-inserts. It catchesNpgsql.PostgresExceptionwithSqlState == PostgresErrorCodes.UniqueViolation(23505) and rethrows asBusinessException(EmailExists). The race is closed atomically by the index.RegisterDevicewas refactored to delegate the row insert toRegisterUser(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 surfaceBusinessException(EmailExists); the caller can retry.
- Added migration
- 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-162Azaion.Services/UserService.cs:84-89(assemblesRegisterDeviceResponse)
- 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-cacheon 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: trueheader so log scrubbers can match-and-mask.
- Set response headers
F-5: EncryptionMasterKey validation is lazy — first failing request, not startup (MEDIUM — CLOSED)
- Severity: Medium
- Status: CLOSED (2026-05-13) —
ResourcesConfig.EncryptionMasterKeyand theResourceUpdateService.MasterKeygetter were both deleted along with the OTA feature (see F-1). The lazy-validation surface no longer exists;appsettings.jsonanddocker-compose.test.ymlno 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.0defaults toroot. There is noUSERdirective afterFROM 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 appto the final stage (the .NET 10 base image already provisions a non-rootappuser, 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 forRegisterUser,ValidateUser,RegisterDevice(cycle-1 added theRegisterDevicecall 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_hashcolumn 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
/logincan brute-force credentials. ASP.NET Core 10 shipsAddRateLimiter()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 classAzaion.Common/Requests/SetUserQueueOffsetsRequest.cs— no validator class
- Description: Other request DTOs use
AbstractValidator<T>(RegisterUserValidator,GetUpdateValidator, etc.). These two are unguarded —LoginRequestaccepts any-length email/password,SetUserQueueOffsetsRequestaccepts any email shape and any offsets payload. - Remediation: Add validators with
EmailAddress()+MinimumLength(12)(matchingRegisterUserValidator) and bounds checks forOffsets.
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.jsonorAzaion.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 atenv/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 to01_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
FileNameis 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— noapp.UseHttpsRedirection(), noHsts. - 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
- All source directories scanned (
Azaion.AdminApi/,Azaion.Services/,Azaion.Common/,env/db/,Dockerfile) - Each finding has file path and (where relevant) line numbers
- No false positives from test files or comments — test-fixture credentials (F-10) are explicitly framed as accepted-risk