Skip to main content

Security Architecture

This document summarizes the current security posture of the MTRO PRO platform and the active audit backlog. It is written for engineering, product, and operations stakeholders who need a single reference for how the platform protects tenant data, guest data, payments, leases, and messaging.

Last updated: 2026-05-19.

Scope

The platform is composed of these security-relevant services:

  • core: the backend API and the main authorization boundary.
  • admin-panel: the property-manager dashboard.
  • guest-app: the guest-facing application.
  • superadmin-panel: internal operations panel.
  • landing: public property and entity discovery pages.
  • pdf-converter: local document conversion service for PDF, HTML, and Word workflows.
  • ff-scraper: Furnished Finder import microservice.
  • url-shortener: public payment-link redirect service.
  • automation: internal scripts, billing tracker, and migration tooling.

The backend API is the source of truth for access decisions. Frontend hiding or disabling is treated as UX only, never as the enforcement layer.

Threat Model

Primary assets:

  • User and guest accounts, including session and refresh tokens.
  • Entity, property, booking, lease, task, payment, vendor, lead, and conversation data.
  • Stripe Connect account references and payment records.
  • Uploaded files and generated lease PDFs in DigitalOcean Spaces.
  • Email, SMS, WhatsApp, and in-app notification history.
  • Internal admin and superadmin capabilities.

Primary attacker classes:

  • Unauthenticated internet users hitting public endpoints.
  • Authenticated guests trying to access owner or another guest's data.
  • Team members with restricted property access trying to access another property.
  • Entity users trying to cross tenant boundaries.
  • Compromised webhook callers or spoofed provider callbacks.
  • Abusive users attempting account takeover, brute force, SSRF, or file-upload attacks.

Authentication

Users and Guests

core supports two authenticated user classes:

  • Dashboard users from database/schemas/users.ts.
  • Guest users from database/schemas/guests.ts.

Login endpoints are rate-limited by IP and email through authLimiter (5 / 15 min). Password recovery endpoints are rate-limited by recoveryLimiter (3 / hour).

Passwords are stored with bcrypt using cost 12. The legacy SHA-256 password format is accepted only during login/change flows and is opportunistically re-hashed to bcrypt after a successful verification.

Sessions

New session tokens use AES-256-GCM authenticated encryption in libs/jwt.ts. The token format is:

v2.<iv-base64url>.<tag-base64url>.<ciphertext-base64url>

validateSession() in libs/crypto.ts accepts only the v2 format. Legacy AES-CBC session tokens are rejected after the MTR-997 cleanup. Refresh tokens are stored server-side on the user/guest record. Access tokens are rejected if expired, and expired refresh tokens are removed when encountered.

API Keys

API keys use the mtro_ prefix and are validated in validateSession(). Current API keys store:

  • keyPrefix for indexed lookup.
  • keyHash for bcrypt comparison.
  • entityId for active-entity scoping.
  • active, expiresAt, and lastUsed metadata.

Legacy plaintext API-key rows are accepted during rollover and are upgraded to the hashed format when first used.

Authorization

Authorization is enforced server-side. The central permission grid lives in core/libs/access-control.ts and exposes capabilities to the frontend so the UI can hide unavailable actions.

Current entity roles:

  • property_manager
  • virtual_assistant
  • maintenance_staff
  • property_owner

Superadmins (user.role === "admin") bypass entity-role checks. Guests are denied access to owner-side resources unless a route explicitly implements a guest flow.

Property-level isolation uses:

  • propertyAccess: "all" | "selected"
  • allowedPropertyIds: string[]
  • helper functions in core/libs/property-access.ts

Routes that operate on property-scoped resources should use requireResourceAccess(user, action, res, { entityId, propertyId }) or an equivalent explicit check. The frontend must not be considered authoritative.

Tenant Isolation

Tenant isolation is generally enforced through the authenticated user's active entity and, when applicable, property access:

  • validateSession() projects the active Membership onto legacy fields (entity, entityRole, propertyAccess, allowedPropertyIds) so old route code reads the correct active-entity context.
  • Multi-entity sessions carry activeEntityId.
  • API keys are scoped to the entity active when they were created.
  • Property-scoped notification fan-outs call property-access helpers so users only receive booking/payment/conversation notifications for properties they can access.

Public Endpoints

Public endpoints exist for explicit product flows only:

  • Health/status endpoints.
  • Login, refresh, recovery, registration, invitation, and verification flows.
  • Public property/entity/badge/availability pages for the landing site.
  • Token-based lease signing.
  • Public Stripe checkout redirect wrapper.
  • Provider webhooks after signature verification.
  • URL-shortener redirects.
  • pdf-converter file retrieval by UUID for converted JPG outputs.

Public endpoints should either be read-only, token-bound, provider-signed, rate-limited, or explicitly documented as public.

API Hardening

core/app.ts applies platform-wide middleware:

  • CORS allowlist from CORS_ORIGINS or the configured frontend URLs.
  • Localhost allowance for development.
  • JSON and URL-encoded body parsing with 50mb limit.
  • express-mongo-sanitize for NoSQL operator stripping.
  • production error redaction through redactErrorReasonsMiddleware.
  • request timeout extension for upload-heavy and document-rendering endpoints.
  • reverse-proxy trust for DigitalOcean App Platform rate-limit correctness.

Rate Limiting

Central rate-limit policies live in core/libs/rate-limit.ts:

  • authLimiter: login-sensitive endpoints.
  • recoveryLimiter: password recovery.
  • checkoutLimiter: public Stripe checkout wrapper.
  • signTokenLimiter: token-based lease signing.
  • publicLimiter: expensive public landing endpoints.

The test environment disables these limiters to avoid flakes.

MTR-994 audit note: verification-code send and verify endpoints now have route-level limiters, and per-account failed-attempt counters lock the active email or mobile code after repeated misses until a new code is issued.

File Uploads and Object Storage

core/routes/upload.ts uses memory-backed multer with:

  • 10 MB per file.
  • image MIME pre-filter.
  • magic-byte validation for PNG, JPEG, GIF, and WEBP.
  • explicit SVG rejection.
  • entity-scoped Spaces keys: uploads/{entityId}/{uuid.ext}.

Uploads default to public unless isPublic=false is supplied. Private objects are returned as private://bucket/key; access requires /upload/retrieve, which validates that the authenticated user belongs to the entity encoded in the object key and returns a 1-hour signed URL.

Webhooks

Resend

/webhooks/resend verifies Svix headers using the raw request body captured in app.ts. This route must remain behind the raw-body capture logic; otherwise signature verification becomes unreliable.

Twilio

Twilio inbound SMS and status callbacks call verifyTwilioRequest() from core/libs/twilio-signature.ts, which validates X-Twilio-Signature against the public webhook URL. Invalid or missing signatures return 403.

Stripe

Payments are created through Stripe Checkout. Card and ACH details are handled by Stripe; MTRO PRO stores payment metadata, Checkout session IDs, routing references, and Stripe Connect account IDs, not raw payment credentials.

Payments

Payment security controls:

  • Stripe Checkout is used for card and ACH collection.
  • The public payment URL points at /payments/checkout/:id, which can refresh expired Checkout sessions instead of exposing raw Stripe session URLs as the long-lived source of truth.
  • Payment routing resolves through a central Stripe account resolver.
  • Stripe Connect account management is owner/superadmin gated.
  • The platform fee is applied through Stripe application_fee_amount.
  • Payment lists are entity-scoped and property-filtered for selected-property users.

Documents and Lease Rendering

Lease rendering uses the local pdf-converter service, not an external document conversion SaaS.

Important controls:

  • core calls the converter with PDF_CONVERTER_API_KEY when configured.
  • Lease merge fields are HTML-escaped before substitution.
  • Word imports are proxied through core/routes/templates.ts with a 25 MB multer limit.

Open audit items:

  • pdf-converter currently allows conversion endpoints without auth if API_KEY is unset. Production must always set API_KEY; the service should be hardened to fail closed unless an explicit local-dev override is present.
  • Direct pdf-converter upload endpoints need explicit file-size limits.
  • WeasyPrint may fetch external URLs referenced by user-controlled HTML; URL fetch restrictions should be reviewed before exposing the converter outside a private network.

Messaging and Notifications

Conversation messaging supports in-app, email, SMS, and WhatsApp channels.

Security-relevant controls:

  • Conversation access is entity-scoped and property-scoped for booking conversations.
  • Lead and vendor conversations retain their entity-level routing.
  • Reply-by-email uses conversation-specific aliases.
  • Inbound SMS routing uses Twilio signature verification and ambiguity rejection when a phone number maps to multiple possible conversations.
  • Delivery status tracking records provider failures instead of silently assuming success.

MTR-990 audit note: raw conversation-message email bodies, webhook-generated conversation notifications, task notes, manual payment reminder text, and campaign email text now pass through a shared HTML-escaping helper before newline conversion.

Frontend Security

Frontend applications should assume all authorization decisions can be bypassed client-side. Current controls:

  • Access capabilities are exposed by the backend so the UI can hide unavailable features.
  • Protected routes redirect unauthenticated users and first-time password change users.
  • Lease PDFs are rendered in browser-native iframes from server-generated URLs.
  • Dashboard Creator scripts are validated by the API before create/update and before returning AI-generated code.
  • Dashboard Creator execution in admin-panel and superadmin-panel runs in a dedicated Web Worker with a 3-second timeout and runtime shadowing for network, storage, DOM, dynamic-code, messaging, and browser-side exfiltration APIs. The worker is terminated after every execution.

MTR-1001 frontend sink review:

  • admin-panel/src/components/ui/chart.tsx, guest-app/src/components/ui/chart.tsx, and superadmin-panel/src/components/ui/chart.tsx use dangerouslySetInnerHTML only to emit CSS custom properties from local chart config. No user HTML is injected through those sinks.
  • landing/src/pages/OwnerProperties.tsx no longer renders owner biography as HTML; it strips markup and renders text, matching the property detail page.
  • admin-panel iframe usage is limited to lease/template PDF previews from server-generated URLs. Those views should remain PDF-only; do not point these iframes at user-controlled HTML.

Deployed header inventory checked on 2026-05-19:

AppEnvironmentCSPHSTSX-Frame-OptionsNotes
admin-paneldev/prodabsentabsentabsentDigitalOcean static site behind Cloudflare.
guest-appdev/prodabsentabsentabsentDigitalOcean static site behind Cloudflare.
superadmin-paneldev/prodabsentabsentabsentDigitalOcean static site behind Cloudflare.
landingdev/prodreport-only after MTR-1001 deployabsentabsentExpress SSR service now disables X-Powered-By and sets baseline response headers.

Recommended next header step: add matching response headers for the static frontends at the deployment edge and promote CSP from report-only to enforcing after validating custom domains. A strict enforcing CSP must account for Google Fonts, Google Maps, Sentry, Stripe, PDF previews, and the Dashboard Creator worker execution model.

Logging and Audit Trail

Core actions are logged through core/libs/logger.ts into the logs collection. Security-relevant events include:

  • user creation, login, password recovery, password reset, activation, and impersonation token generation.
  • team and membership changes.
  • API key creation/revocation/deletion.
  • file uploads and private file accesses.
  • payment lifecycle events.
  • Stripe account lifecycle events.
  • conversation message delivery, retry, edit, and delete events.
  • task assignment and assignee notification events.

Production error responses redact internal reason, stack, and trace fields.

MTR-990 Audit Remediations

The first audit pass for MTR-990 fixed these issues:

  • POST /users/create now rejects role: "admin" unless the internal secret is supplied. Previously a missing secret was accepted because only the wrong-secret path was blocked.
  • POST /users/change-password now requires an authenticated matching user session, or a superadmin session.
  • POST /guests/change-password now requires an authenticated matching guest session.
  • POST /guests/change-password-first-time now requires an authenticated matching guest session.
  • User-controlled message bodies in conversation replies, inbound SMS/email webhook notifications, task-assignee emails, manual payment reminders, and campaign emails are now HTML-escaped before being inserted into email HTML.
  • Production-code dynamic imports found during the audit were removed from the admin report PDF export and the Furnished Finder profile diff script.
  • POST /users/send-verification-code and POST /users/verify-code now have route-level rate limiters plus per-user email/mobile attempt counters.
  • pdf-converter conversion endpoints fail closed without API_KEY unless PDF_CONVERTER_ALLOW_UNAUTHENTICATED_LOCAL_DEV=true is set explicitly, enforce request/upload/payload size limits, and reject remote resources in WeasyPrint HTML-to-PDF inputs.
  • url-shortener now validates redirect targets before creation and before redirecting stored links. Targets must use https, contain no URL credentials, and match the explicit payment-link host allowlist.

Regression tests were added for these paths.

Active Audit Backlog

The following items remain open after the first MTR-990 pass:

  1. Review every route still using hand-written authorization instead of requireResourceAccess() and migrate high-risk resources first.
  2. Implement frontend security headers after validating CSP in report-only mode.
  3. Continue dependency and container audits for core, pdf-converter, ff-scraper, and frontend builds.
  4. Continue periodic workspace hygiene sweeps so local tool-history and agent-settings files do not retain database/provider credentials.

Dependency Audit Snapshot

Production dependency audits were run on 2026-05-18.

Clean production audit:

  • core
  • url-shortener
  • guest-app
  • landing
  • automation
  • mcp
  • virtual-otp
  • ff-scraper
  • docs
  • portable-reputation
  • prototypes/lease-flow-co-pilot
  • prototypes/lease-leap-app

Remediated production dependency findings:

  • admin-panel, guest-app, and superadmin-panel: direct jspdf dependency upgraded from 3.0.3 to 4.2.1, removing the critical jsPDF advisories from the production audit.
  • MTRO-managed JavaScript packages have been standardized on pnpm; npm lock files remain only in vendored OpenZeppelin contract dependencies.
  • admin-panel, guest-app, superadmin-panel, and landing: React Router and @remix-run/router are pinned or overridden to patched releases.
  • admin-panel, guest-app, and superadmin-panel: vulnerable lodash, lodash-es, DOMPurify, Tailwind/PostCSS transitive packages, and globbing transitive packages are patched through direct upgrades or pnpm.overrides.
  • automation, mcp, virtual-otp, and ff-scraper: axios, follow-redirects, SDK, and routing transitive findings are patched through direct upgrades or pnpm.overrides.
  • docs: Docusaurus/Webpack audit findings are patched through pnpm.overrides; Webpack is pinned to 5.105.4 for Docusaurus 3.9.2 compatibility.

Open production dependency findings:

  • admin-panel and superadmin-panel: react-quill depends on quill versions affected by a moderate XSS advisory. The advisory has no patched quill release in the current dependency line (patched_versions <0.0.0), so this requires replacing or isolating the rich-text editor rather than a lockfile patch.

These findings are dependency-maintenance work. They are separate from the backend authentication fixes applied during MTR-990.

Developer Workspace Secret Hygiene

Developer workspaces often accumulate tool state outside the application repositories. These files can contain commands, pasted payloads, model context, or shell snapshots that include credentials even when the repository itself is clean.

Local-only tool state must not be committed, synced, archived, or copied into issue comments. The current ignore policy excludes:

  • .claude/
  • .codex/
  • .agents/
  • .grog/
  • .env.* files, while keeping .env.example trackable.
  • *.local.json

Expected handling workflow:

  1. Store real credentials only in the relevant secret manager or intentionally local runtime files that are ignored by git.
  2. Do not paste production database URIs, provider API keys, webhook signing secrets, private keys, or session tokens into agent prompts, Linear comments, GitHub comments, or generated docs.
  3. Before archiving or syncing a workspace, delete local tool-history/cache directories such as ~/.claude/file-history, ~/.claude/projects, ~/.claude/paste-cache, ~/.claude/shell-snapshots, ~/.codex/sessions, ~/.codex/history.jsonl, and transient .tmp tool caches.
  4. Run a redacted secret scan that reports file paths and match classes only; never paste detected secret values into tickets or chat.
  5. If a real credential is found in a tracked file, external comment, shared archive, or any other non-local medium, rotate it immediately in the source provider and update the secret manager reference.

MTR-999 cleanup note: local agent history/cache files were purged on 2026-05-18. No tracked MTRO repository file contained a real provider token or database credential requiring rotation; active local-only tool credentials were left in place and must stay out of archives.

Operational Checklist

Before production deploys:

  • Confirm SECRET is set and consistent only within the intended environment.
  • Confirm CORS_ORIGINS or frontend URL env vars are populated.
  • Confirm PDF_CONVERTER_API_KEY, FF_SCRAPER_API_KEY, and shortener API_KEY are set.
  • Confirm Resend and Twilio webhook secrets are set.
  • Confirm Stripe keys are environment-specific.
  • Confirm DigitalOcean Spaces credentials are stored as secrets.
  • Confirm RATE_LIMIT_DISABLED is not set in production.
  • Confirm logs and Sentry do not include raw passwords, tokens, or provider secrets.
  • Confirm local tool configuration and shell history do not contain production database URIs or provider tokens.