Vistralio Security Review
Date: 2026-04-14
Build reviewed: 0.1.0 (Beta) Build 20260414.3
Scope
This review was a targeted application hardening pass over the current Vistralio codebase and deployment layout. It covered the main web UI, API, camera configuration paths, activity logging, runtime path handling, and support/admin surfaces.
This document does not claim the system is free from unknown vulnerabilities or "zero-day" issues. It records what was checked, what was fixed, and what residual risks remain after this pass.
Areas reviewed
- Authentication and JWT handling
- Permission enforcement on admin and support endpoints
- Camera configuration and secret exposure
- Activity/audit logging of WebUI actions
- Runtime storage path overrides and cleanup tooling
- Media/download endpoints using browser-compatible token auth
- Build integrity and backend syntax validation
- Duplicate route surface and accidental endpoint drift
- Dependency vulnerability audit and package remediation
Issues found and fixed
1. Camera credentials were exposed back to the frontend
Issue:
- The camera API returned stored camera and ONVIF passwords in normal API responses.
- Any user with camera access could receive raw saved credentials in the web UI payload.
Fix:
- Camera and ONVIF passwords are no longer returned by the API.
- The API now returns
has_passwordflags instead. - The camera settings UI was updated to preserve existing saved passwords unless an operator explicitly enters a replacement.
- The per-camera full settings endpoint now requires
cameras.editrather than plaincameras.view.
Files:
backend/app/api/cameras.pyfrontend/src/pages/CameraConfig.jsxfrontend/src/pages/Admin.jsx
2. WebUI activity logging could store secrets
Issue:
- The activity log was capturing full before/after payloads for camera and settings updates.
- That could include passwords, tokens, and similar sensitive values.
Fix:
- Added recursive redaction for sensitive keys before writing activity log entries.
- Redaction now covers common secret-bearing keys such as passwords, tokens, authorization headers, JWTs, and similar fields.
Files:
backend/app/services/activity.py
3. JWT error messages leaked parsing details
Issue:
- Invalid token responses included the underlying JWT parser error text.
Fix:
- Token failures now return a generic
Invalid tokenresponse instead of reflecting parse/signature details back to the client.
Files:
backend/app/core/security.py
4. Runtime storage overrides could point at dangerous system roots
Issue:
- Runtime path overrides accepted arbitrary strings from settings.
- A bad path value could cause metrics, cleanup, exports, clips, or recordings features to operate against unsafe system locations.
Fix:
- Runtime paths are now normalized to absolute paths.
- Runtime paths are now restricted to approved filesystem roots.
- The live system was seeded with
system.path.allowed_rootsso the allowlist is explicit and operator-controlled. - Relative paths and paths outside approved roots are rejected and replaced by safe defaults.
Files:
backend/app/core/runtime_paths.pybackend/app/api/system.py
5. Media URLs used the primary login JWT in query strings
Issue:
- Browser-facing media URLs used the main login token directly in
?token=...query parameters for images, recordings, and live WebSocket feeds.
Fix:
- Added a separate short-lived media token flow via
/api/auth/media-token. - The frontend now refreshes and uses media-only tokens for browser media URLs instead of the main session token.
- Media endpoints now accept media tokens as well as regular authenticated access where appropriate.
Files:
backend/app/core/security.pybackend/app/api/auth.pybackend/app/api/streams.pybackend/app/api/recordings.pyfrontend/src/api.jsfrontend/src/App.jsxfrontend/src/pages/Cameras.jsx
6. Secrets were not encrypted at rest in the application database
Issue:
- Sensitive values such as camera credentials, ONVIF credentials, SMTP password, and the applied license key could remain stored in plaintext in the database.
Fix:
- Added transparent encrypted-at-rest handling for selected secret-bearing fields.
- Existing plaintext values are migrated on startup into encrypted form.
- Reads remain backward-compatible for any older values encountered before migration.
- New installs now write a dedicated
security.encryption_secretinto the main config file, while existing installs fall back safely to the existing JWT secret if needed.
Files:
backend/app/core/secrets.pybackend/app/models/all.pybackend/app/api/cameras.pybackend/app/services/onvif_client.pybackend/app/services/smtp.pybackend/app/services/licensing.pybackend/app/api/settings.pybackend/app/main.pyscripts/install.sh
7. Duplicate export route definitions
Issue:
- The recordings API contained duplicate
GET /api/recordings/exportdefinitions. - This was not an active exploit by itself, but it increased maintenance risk and made route behavior less trustworthy during future changes.
Fix:
- Removed the duplicate route definitions and kept a single canonical export listing endpoint.
Files:
backend/app/api/recordings.py
8. Vulnerable backend dependency pins
Issue:
- The pinned backend dependency set included multiple packages with known published vulnerabilities.
Fix:
- Updated backend package pins to patched releases:
fastapi==0.135.3starlette==0.49.1cryptography==46.0.7python-jose[cryptography]==3.5.0python-multipart==0.0.26jinja2==3.1.6
Files:
backend/requirements.txt
Checks performed
Static and code-level review
Reviewed:
backend/app/core/security.pybackend/app/api/cameras.pybackend/app/api/recordings.pybackend/app/api/system.pybackend/app/core/runtime_paths.pybackend/app/services/activity.py- relevant frontend camera/admin configuration flows
Focus areas:
- credential exposure
- route protection
- token handling
- path safety
- secret leakage into logs
Verification commands
Backend syntax validation:
env PYTHONPYCACHEPREFIX=/tmp/pycache python3 -m py_compile \
/opt/vistralio/backend/app/core/security.py \
/opt/vistralio/backend/app/services/activity.py \
/opt/vistralio/backend/app/core/runtime_paths.py \
/opt/vistralio/backend/app/api/cameras.py \
/opt/vistralio/backend/app/api/recordings.py \
/opt/vistralio/backend/app/api/system.py \
/opt/vistralio/backend/app/main.py \
/opt/vistralio/backend/app/api/activity.py \
/opt/vistralio/backend/app/api/settings.py \
/opt/vistralio/backend/app/models/all.py \
/opt/vistralio/backend/app/worker.py
Frontend production build:
cd /opt/vistralio/frontend && npm run build
Targeted runtime checks:
- verified path sanitization behavior with the project virtualenv
- verified camera serialization no longer returns raw password fields
- verified activity-log redaction removes secret-bearing values
- verified secret encryption/decryption helpers and short-lived media token decoding
Dependency audit checks:
cd /opt/vistralio/frontend && npm audit --omit=dev --json
/tmp/pip-audit-sentinel/bin/pip-audit -r /opt/vistralio/backend/requirements.txt
Results after remediation:
- frontend production dependency audit:
0vulnerabilities - backend requirements audit:
No known vulnerabilities found
Residual risks and follow-up items
These items were identified but are not fully eliminated by this pass:
Query-string media authentication still exists in a reduced-risk form
The app still uses ?token=... on image/video/WebSocket media URLs where the
browser cannot attach an Authorization header directly. This is common for
browser media delivery, but it now uses short-lived media-only tokens rather
than the main session JWT. Residual risks still include:
- token exposure in browser history
- token exposure in copied URLs
- token exposure via intermediate logging if a proxy is misconfigured
Recommended follow-up:
- move media delivery to cookie-backed or one-time signed URLs for the highest-security mode
Not every secret-bearing surface is fully redesigned yet
This pass added encrypted-at-rest handling for selected DB-backed application secrets, but not every bearer-style operational secret was redesigned. For example, some enrollment/rotation flows still rely on sensitive tokens that are intentionally shown to privileged administrators when needed.
Recommended follow-up:
- extend the same pattern to any remaining bearer-style enrollment tokens if you want them unreadable at rest too
- rotate existing operational secrets after a major security maintenance window
Admin path configuration remains an admin trust boundary
Runtime path overrides are now restricted to approved roots, but an administrator can still expand that allowlist deliberately. This is expected behavior for system administration.
Recommended follow-up:
- add a dedicated UI for approved storage roots if you want this managed without direct settings-key edits
No formal external penetration test or OS hardening review was run in this pass
This review focused on application logic and configuration exposure. It did not include:
- external penetration testing
- SAST/DAST tooling integration
- OS-level hardening review of nginx/systemd/MariaDB
Recommended follow-up:
- add automated dependency scanning and scheduled external testing
Outcome summary
The main concrete security fixes from this pass are:
- camera secrets are no longer returned to the frontend
- activity logs no longer store secret values
- token error responses no longer leak parser detail
- unsafe runtime path overrides are constrained to approved roots
- browser media URLs use short-lived media-only tokens instead of the main JWT
- selected DB-backed secrets are now encrypted at rest
- duplicate route surface was cleaned up
- vulnerable backend package pins were upgraded
No evidence of an active credential leak was found in the reviewed paths after these changes, but this should be treated as an ongoing hardening process, not as a guarantee that all unknown vulnerabilities have been eliminated.