Skip to content

Security

mycashless is a payment platform — cardholder PII and stored balance are the crown jewels. Card capture is delegated end-to-end to PCI-validated processor iframes and SDKs (Stripe, Adyen, Mercado Pago, PayPal), so no PAN ever lands in our application code, logs, or database. What we do hold — account identity, balance, transaction history, refund payout details — is gated behind tenant-aware FastAPI dependencies, scoped JWTs from cashless-auth, and an audit trail in MySQL. This page is the public-facing posture summary; the per-topic audits it links to are the source of truth.


Threat model

  • Actors

    Cardholders, venue operators (User), platform admins (Admin), paired POS devices, cluster cron, anonymous attackers.

  • Assets

    Cardholder PII, stored wallet balance, refund-payout instruments, operator + admin credentials, processor secrets.

  • Trust boundaries

    Browser / mobile app ↔ GCE Ingress ↔ FastAPI service ↔ Cloud SQL. Service ↔ GCP Secret Manager via ESO. Service ↔ payment processor over the public internet, signed both ways.

  • Out-of-scope (delegated)

    Card capture (processor iframes / SDKs), card storage (tokenized to pm_* / pi_* / pspReference), card-present EMV decryption (Stripe Terminal P2PE).

The threat model assumes any authenticated actor can attempt to escalate across the tenancy hierarchy (VendorEventUser). Every audit below is framed against that assumption.


Audit status

Each row links to the internal audit document. Status reflects the audit's own conclusion as of its publish date — not a re-test.

Audit Status Top finding Link
OWASP Top 10 consolidation ⚠ gaps B− posture; legacy operator-token shape and outdated cryptography pin flagged for P0 remediation. security-audit-2026-05.md
Authorization (AuthZ) ✅ clean 599+ endpoints surveyed; zero missing-auth endpoints — every route either authenticated or intentionally public with a rate limit. security-authz-audit.md
CSRF ✅ clean Bearer-token (JWT) auth only — CSRF is structurally not applicable. Unused Angular XSRF config flagged as dead code to remove. security-csrf-audit.md
Multi-tenant isolation ⚠ gaps 97.7% of 786 routes verified tenant-safe; localized gaps on admin customer listing and modifier mutation routes. security-multi-tenant-isolation-audit.md
PII redaction ⚠ gaps structlog pipelines lack global PII processors; several AuditLog.new_values writes persist email/phone/IP unredacted. security-pii-redaction-audit.md
Pydantic strict input ⚠ gaps 52.6% of request models declare extra="forbid"; cashless-auth request models at 0% — strictness work scheduled before launch. security-pydantic-strict-audit.md
Rate limit ⚠ gaps cm_v2 fully covered (default 600/min/admin + per-route overrides); pa_v4 and cashless-auth have unlimited endpoints flagged for default-limit rollout. security-rate-limit-audit.md
SQL injection ✅ clean 6,368 Python files scanned, 47 dynamic-SQL patterns triaged — zero unsafe constructions. All text(...) uses bound parameters. security-sql-injection-audit.md
XSS (frontends) ✅ clean 6 [innerHTML] bindings across four Angular apps — all routed through DomSanitizer.sanitize(SecurityContext.HTML, …). No bypassSecurityTrust*, no eval. security-xss-audit.md

Active findings

The ⚠ audits above carry open work items tracked in the consolidated security-audit-2026-05.md remediation table. The summary table at the top of that document lists the next-up P0 / P1 items and rough effort estimates. None of the items are runtime-exploitable in production today (the platform is pre-launch), but they are blockers on the path to launch.


PCI posture

Card data is handled exclusively by PCI-DSS Level 1 processors. Application code only ever sees opaque tokens; no PAN, no CVV, no expiry, no track data crosses any service boundary we own.

Capture surface Tokenization layer What our app receives Scope
Web e-commerce (Stripe) Stripe Elements iframe pm_* payment-method ID SAQ A
Web e-commerce (Adyen) AdyenCheckout drop-in iframe pspReference SAQ A
Web e-commerce (Mercado Pago) MP Bricks / Checkout-Pro hosted page MP payment_id, preference_id SAQ A
Web e-commerce (PayPal) PayPal Smart Buttons hosted iframe PayPal order_id, capture_id SAQ A
Mobile e-commerce (iOS / Android) Stripe SDK PaymentSheet pm_* SAQ A
Apple Pay / Google Pay OS-level DPAN via Stripe Elements pm_* SAQ A
In-person card present (mPOS) Stripe Terminal SDK + P2PE-listed reader pi_* / charge_id SAQ B-IP / P2PE
Inbound processor webhooks Signed HTTPS callbacks (Stripe, Adyen, MP, PayPal HMAC) Token IDs + amounts Out-of-CDE

What lives in our database is intentionally narrow: transaction.last_four (four digits — PCI-permitted display data), stripe_customer_id, payment_method token references, and pspReference-style processor IDs. The recommended SAQ scope is SAQ A for the e-commerce flows and SAQ B-IP (or P2PE if attestation is available) for the mPOS in-person flow — both to be confirmed with the acquirer.

Full PCI walk-through, requirement-by-requirement evidence, and the recommendation queue: pci-posture-2026-05.md (supersedes the earlier security-pci-posture.md).

Pre-launch PCI work

Legacy Flask runtimes (cm/, pa/) still expose refund-payout fields that overlap with PCI scope on paper. The launch cutover plan retires those public routes before SAQ submission and runs the column-drop migration in the same window. Tracked in pci-posture-2026-05.md finding 4.1.


GDPR posture

The platform was designed around LATAM (Argentina, Mexico) but Art. 3(2)(a) extra-territorial scope applies the moment we accept EU data subjects. Lawful-basis mapping is documented per-field; data-subject rights are partially implemented and tracked.

Data-subject right (Art.) Status Surface / endpoint
Art. 15 — access (SAR) ⚠ planned No /v4/customers/me/data-export yet — backlog.
Art. 16 — rectification ✅ partial PUT /v4/users/me for editable fields; name-change goes through an ops workflow.
Art. 17 — erasure ⚠ partial DELETE /v4/users/me exists and nulls top-level PII; cascade-redaction across EventConnection + denormalized Transaction.user_email is open work.
Art. 18 — restriction ⚠ planned Out-of-band via privacy@mycashless.com (TBD — see below).
Art. 20 — portability ⚠ planned Piggy-backs on the Art. 15 export.
Art. 21 — object (marketing) ✅ implemented Customer.send_email / send_sms / send_app_notifications flags; cookie banner on all four frontends with explicit Decline.
Art. 22 — automated decisions n/a No profiling or automated individual decisions.

Lawful-basis matrix (engineering inference; legal counsel owns the authoritative Art. 30 RoPA):

Category Lawful basis
Account identity (name, email, phone, password) Art. 6(1)(b) — contract
Profile (address, language, photo) Art. 6(1)(b) where event-required, otherwise Art. 6(1)(a) — consent
Health-adjacent (allergy) Art. 9(2)(a) — explicit consent (special category)
Emergency contact Art. 6(1)(d) — vital interests
National identifiers (id_number, clabe_account) Art. 6(1)(c) — legal obligation (refund payouts)
Marketing flags Art. 6(1)(a) — consent
Transaction history Art. 6(1)(b) + Art. 6(1)(c) — financial records
Audit / security logs Art. 6(1)(f) — legitimate interests

Full GDPR walk-through, retention-matrix proposal, and DSAR-intake plan: gdpr-posture-2026-05.md (supersedes the earlier security-gdpr-posture.md).

Retention + erasure remediation

No automated retention purge job runs today, and the erasure path only nulls a subset of PII columns. Both are launch-blocking for EU-subject traffic and tracked as HIGH-severity items in gdpr-posture-2026-05.md §6.1 / §6.3.


Tenant isolation model

Tenancy is hierarchical and enforced at the FastAPI dependency layer, not in raw queries:

Vendor
  └─ Event
       └─ User (operator) / Customer (cardholder)
            └─ Transaction · Transfer · Refund · Token · NFC chip

Three primitives cover the matrix:

Dependency Caller identity Scope rule
require_tenant_access Admin Admin must be connected to the target event_id via EventConnection.
require_user_event_access User (operator) Operator JWT is pinned to a single event_id; mismatch → 403.
CurrentCustomer Cardholder Resolves the JWT sub; downstream queries filter by customer.id.

The row-level rule across cm_v2 and pa_v4 is: every state-mutating query loads the target row, derives its event_id (directly or via the nearest parent), and calls the matching require_*_access predicate before mutation. The multi-tenant isolation audit covers 786 routes — security-multi-tenant-isolation-audit.md documents the patterns plus the localized gaps still on the remediation queue.

Customer-scoped reads in pa_v4 follow Pattern 4 of the audit: the CurrentCustomer dependency resolves the cardholder from the JWT sub claim and post-load filters scope to customer.id — there is no admin-style cross-event traversal on the cardholder surface.


Reporting a vulnerability

No published disclosure policy yet

There is no SECURITY.md in the parent monorepo as of 2026-05-12 and no public bug-bounty program. TBD — a security@mycashless.com mailbox + 90-day coordinated-disclosure policy is on the pre-launch backlog and will land here.

In the interim, route security reports through the platform owners on the parent monorepo (Cashless-Media/platform) via a private GitHub Security Advisory rather than a public issue. Do not include exploitable proof-of-concept payloads in any public-facing channel until the policy lands.


See also

  • Architecture — service boundaries, payment-processor inventory, deploy flow.
  • API reference — Swagger / OpenAPI for every authenticated surface.
  • Operations — cluster topology, secret-manager wiring, runbook index.
  • OPERATIONS.md — full cluster runbook.