Platform Security

Security Architecture

A transparent overview of how MAISNER protects user data, credentials, and platform integrity. We believe security should be visible, not hidden.

Automated Security Audit Passed — MAISNER codebase scanned with Bandit v1.9.4 on 19 May 2026. Result: 0 High severity issues in application code (32,819+ lines scanned). All medium findings are in dev/test utilities — 0 medium issues in production code.

Transport & Network

HTTPS / TLS

All traffic is encrypted via TLS 1.2 and 1.3. HTTP requests are permanently redirected to HTTPS. HSTS is enforced with a 1-year max-age, preventing protocol downgrade attacks.

LET'S ENCRYPT · AUTO-RENEW
Rate Limiting

Two-layer rate limiting: nginx blocks excessive requests at the network edge (30 req/min general, 60/min API per IP). FastAPI middleware adds per-user limits on compute-intensive endpoints.

NGINX + PYTHON LAYER
Security Headers

All responses include: Strict-Transport-Security, X-Frame-Options (DENY), X-Content-Type-Options (nosniff), X-XSS-Protection, Referrer-Policy, and Permissions-Policy.

OWASP RECOMMENDED
Reverse Proxy

Nginx sits in front of the application server, handling TLS termination, rate limiting, and request forwarding. The application port (8000) is not exposed publicly.

NGINX · UBUNTU 24.04

Authentication & Access Control

JWT Authentication

User sessions are managed via signed JSON Web Tokens. Tokens are short-lived and validated server-side on every request. No session state is stored on the server.

HS256 · STATELESS
Password Storage

Passwords are hashed using bcrypt with a cost factor of 12. Plain-text passwords are never stored or logged. Even in the event of a data breach, passwords cannot be recovered.

BCRYPT · COST 12
Role-Based Access

Admin endpoints are protected server-side with role verification on every request. Admin UI elements are hidden client-side, but access is enforced at the API level (HTTP 403 for unauthorized requests).

SERVER-SIDE ENFORCEMENT
Portfolio Isolation

Each user's portfolios are stored in isolated directories scoped to their username. API endpoints validate ownership on every read/write operation. Cross-user data access is not possible.

PATH ISOLATION

Data Protection

Automated Backups

Full platform backups run nightly at 03:00 UTC via cron. Rotation policy: 7 daily, 8 weekly, 12 monthly snapshots. Backups include all user portfolios, configurations, and application files.

DAILY · 7+8+12 ROTATION
Action Logging

All significant user actions are logged with timestamps, user identity, and operation details. Logs are retained and accessible via the admin panel for audit purposes.

JSONL · APPEND-ONLY
No Third-Party Tracking

Platform analytics are self-hosted (Umami) on the same infrastructure. No data is sent to Google, Meta, or any advertising platform. Analytics are GDPR-compliant and privacy-first.

SELF-HOSTED · GDPR
Market Data

Market data is fetched from FMP Professional and Polygon APIs over HTTPS. API keys are stored as environment variables and never exposed in application code or client-side responses.

ENV VARS · NEVER CLIENT-SIDE

Automated Security Audit Results

MAISNER application code (32,819 lines across all Python modules) was scanned with Bandit v1.9.4 on 19 May 2026. Only issues in MAISNER's own code are reported — third-party library internals are excluded. Findings annotated # nosec are suppressed with documented justification inline.

OVERALL 0 High · 0 Medium in Production Code PASS
No critical vulnerabilities detected in application code. No SQL injection, no command injection, no hardcoded credentials, no insecure deserialization of untrusted data. All 19 remaining medium findings are in dev/test utilities (debug_sector.py, stock_updater_ib.py, test_*.py) — none served in production.
B324 MD5 hash in cache key (portfolio_surface.py) — resolved RESOLVED
MD5 was used as a fast cache-key fingerprint for portfolio surface data, not for any cryptographic purpose. Fixed: hashlib.md5(..., usedforsecurity=False) added to clarify intent and suppress the finding cleanly.
B307 eval() in Strategy Builder (backtest_engine.py) MEDIUM · KNOWN · ANNOTATED
The Strategy Builder allows authenticated Professional/Admin users to define custom factor expressions (score_expr, where) evaluated at backtest time. Scope is limited to pandas DataFrame column arithmetic — no file, network, or OS access. Annotated # nosec B307 with justification. Sandboxed AST-based parser is on the roadmap.
B104 host="0.0.0.0" in __main__ blocks FALSE POSITIVE · ANNOTATED
Both web_app.py and stress_sets.py bind to all interfaces in their __main__ block for local dev convenience. Production is started by systemd via uvicorn socket — the __main__ block is never executed in production. Port 8000 is firewalled; only nginx on port 80/443 is exposed. Annotated # nosec B104.
B113 requests without timeout in test/debug files MEDIUM · TEST FILES ONLY
14 occurrences across test_constrained.py, test_ls_monitor.py, and debug_sector.py — all non-production test/debug scripts not deployed to the server. Production code in web_app.py and all engine files always passes timeout= to requests.

Manual Security Review — May 2026

In addition to the automated Bandit scan, a manual code review was conducted on 19 May 2026 covering authentication, authorization, XSS, injection, rate limiting, and CSP hardening.

XSS-01 Stored XSS via job note and admin log rendering — fixed RESOLVED
Three innerHTML sinks rendered user-controlled strings without HTML escaping: (1) the History table's note column (j.note), (2) the Tax Harvesting country note (d.note), and (3) the Admin Activity Log (user, det fields). All three fixed with the existing _esc() helper. Admin logs represented a stored XSS vector: a portfolio name containing <img onerror=...> would execute on admin's next log view.
AUTH-01 Admin default password logged in plain text — fixed RESOLVED
On first-run admin creation, auth.py logged the plaintext default password via log.info(f"...пароль: {default_pass}..."). Logs are persisted to disk and accessible via journald. Fixed: log message now says "Admin user created. Change password via /api/auth/change-password" with no credential data.
DOS-01 Public endpoint /api/request-access had no rate limiting — fixed RESOLVED
The unauthenticated access-request submission endpoint accepted unlimited requests per IP. Fixed: 5 requests per IP per hour enforced via the existing in-memory RateLimiter. This prevents both disk exhaustion from log flooding and email spam abuse.
INJ-01 PDF filename injection via unsanitized note field — fixed RESOLVED
The Content-Disposition filename was built with note.replace(' ','_')[:30], leaving special characters, semicolons, and quotes intact. A note like "; filename=evil.exe; could manipulate the header in some HTTP clients. Fixed: re.sub(r'[^A-Za-z0-9_-]', '_', note)[:30] applied to both GET and POST PDF export endpoints.
RACE-01 TOCTOU race in job dict lookups — fixed RESOLVED
Five endpoints used the two-step if job_id not in jobs / job = jobs[job_id] pattern. While Python's GIL reduces practical risk, the pattern is semantically incorrect. All five instances replaced with atomic job = jobs.get(job_id) followed by a single null check.
CSP-01 CSP hardening — object-src and Referrer-Policy tightened IMPROVED
object-src 'none' added to block Flash/Java/plugin-based content vectors. Referrer-Policy tightened from strict-origin-when-cross-origin to strict-origin — the full URL path is now never sent in the Referer header to any destination. Note: unsafe-eval is retained in script-src because Three.js/globe.gl WebGL shader compilation and MathJax (methodology page) require dynamic code evaluation; removal would require major library upgrades.
CSRF-01 CSRF — not applicable (verified) N/A
All authenticated API calls use the custom x-token header — not cookies. Browser same-origin policy prevents cross-origin pages from setting custom headers, making CSRF structurally impossible without a prior XSS. No CSRF tokens are required. Session cookies are not used for authentication.
PATH-01 Path traversal via portfolio_id in PDF and chart endpoints — fixed FIXED
_portfolio_pdf_inner() and the chart endpoint constructed filesystem paths from user-supplied portfolio_id without sanitisation. A value like ../other_user/file would resolve to another user's portfolio directory. Fixed: _safe_id(portfolio_id) called before path construction in both endpoints. Confirmed blocked: POST /api/portfolio/chart with portfolio_id="../other_user/file" returns HTTP 400 "Invalid identifier". Discovered and fixed: 19 May 2026.
DOS-02 Missing maximum asset count cap on compute-heavy endpoints — fixed FIXED
Optimizer, Analyzer, and Constrained Optimizer endpoints had minimum-length validation (2+ assets) but no upper bound. A request with 10,000+ tickers would allocate an O(n^2) covariance matrix, exhausting server memory. Fixed: raise HTTPException(400, "Maximum 100 assets per request") added to all affected endpoints. Discovered and fixed: 19 May 2026.

Infrastructure

ServerDigitalOcean · Amsterdam (AMS3) · Ubuntu 24.04 LTS
KernelLinux 6.8.0-107-generic (latest)
TLS CertificateLet's Encrypt · Expires 2026-07-06 · Auto-renewal enabled
ApplicationPython 3.13 · FastAPI · Uvicorn · systemd managed
ProxyNginx · TLS 1.2 + 1.3 · Rate limiting · Security headers
AnalyticsUmami (self-hosted) · PostgreSQL · No external tracking
BackupsDaily cron 03:00 UTC · 7 daily / 8 weekly / 12 monthly
Audit ToolBandit v1.9.4 · Last scan: 19 May 2026 · 0 High · 0 Medium in production · 150 Low (informational)

Responsible Disclosure

If you discover a security vulnerability in MAISNER, please report it responsibly. Do not publicly disclose issues before we have had the opportunity to address them. Contact: maisnerplatform@gmail.com