Security model
CDA Custom Fields handles potentially-sensitive customer data (VAT IDs, addresses, contact details, file uploads). This page covers the security architecture for security-conscious merchants and auditors.
Threat model
Section titled “Threat model”CDA is designed to resist:
| Threat | Mitigation |
|---|---|
| SQL injection on field values | Parameterized queries throughout; no string interpolation into WHERE/SELECT |
| XSS in admin / storefront / email rendering | Every output passes through Magento’s Escaper (escapeHtml, escapeUrl, escapeHtmlAttr) |
| CSRF on admin saves | Magento’s standard form_key validation on every POST controller; explicit CsrfAwareActionInterface on JSON-body endpoints |
| File upload abuse (RCE via spoofed MIME) | Magic-byte MIME validation via finfo. Extension reverse-mapped from validated MIME. |
| Path traversal in stored file paths | Storage paths are server-generated (yyyy/mm/<32-hex>.<ext>) and never derived from user input. Strict regex validation on retrieval. |
| Unauthorized file downloads | HMAC-SHA256 signed URLs + 4-tier ownership ladder (admin / order owner / current quote / session-pinned) |
| Mass assignment via inline admin grid | Whitelist of grid-editable columns; non-whitelisted POST keys silently dropped |
| Privilege escalation via wrong ACL | Each admin controller declares its specific ACL resource (field_save, field_delete, templates_apply) — not the generic manage |
| Webhook replay (Stripe → license dupes) | ProcessedStripeEvent idempotency table — duplicate event.id returns 200 without re-creating license |
| License credential leak | License key stored encrypted via Magento\Framework\Encryption\EncryptorInterface (uses installation crypt key) |
Input validation
Section titled “Input validation”Field values go through type-appropriate normalization on save:
- Numeric fields cast to
(int)/(float)before DB write - Multiselect / checkbox arrays serialized via
Magento\Framework\Serialize\Serializer\Json(rejects non-array shapes) - File metadata validated as structured array with required keys before serialization
- String fields stored verbatim — escaping happens at OUTPUT time (
$escaper->escapeHtml($value)), not input time, so we don’t double-escape on display
File upload security
Section titled “File upload security”The file upload flow is the most sensitive surface. Each step is hardened:
1. Authentication
Section titled “1. Authentication”POST endpoint requires a valid form_key. JSON-body endpoints (used by Hyvä Magewire) implement CsrfAwareActionInterface and validate the form_key from a URL query parameter (Magento’s default validator only inspects POST body / header, not query — so we explicitly check).
2. MIME validation
Section titled “2. MIME validation”$finfo = finfo_open(FILEINFO_MIME_TYPE);$mime = finfo_file($finfo, $tmpPath); // reads magic bytesWe never trust:
$_FILES['type'](client-controlled)- The file extension (client-controlled)
- Any header sent with the upload
The configurable allow-list is matched against the finfo-detected MIME. The saved filename’s extension is reverse-derived from the validated MIME using a server-side table (image/jpeg → jpg, etc.) — so a .exe renamed to .jpg becomes a .bin rejected.
3. Storage path
Section titled “3. Storage path”pub/media/cda_custom_fields/<yyyy>/<mm>/<32-hex-random>.<validated-ext>- Year/month folders for natural rotation and orphan cleanup
- 32-hex random base name (16 bytes from
random_bytes) — unguessable - Extension always from the server-side MIME table
4. Size limit
Section titled “4. Size limit”Default 10MB per file. Configurable per store. Magento’s upload_max_filesize / post_max_size PHP limits still apply as outer envelope.
5. HMAC-signed download URLs
Section titled “5. HMAC-signed download URLs”Every download URL has:
?p=<rel-path>&e=<expires-timestamp>&s=<HMAC-SHA256>The signature is over path|expires using a per-installation secret derived as:
hash('sha256', 'CDA_CustomFields|file_download|' . $installation_crypt_key)The crypt key is hashed with a module-scoped salt so the signing key is never the raw crypt key — leaking a signed URL never leaks the crypt key.
Signature verification uses hash_equals() for constant-time comparison (resists timing oracles).
6. Ownership check
Section titled “6. Ownership check”A valid HMAC signature alone is NOT sufficient. The Download controller layers an authorization check:
- Admin session — full access (admins see every order’s attachments)
- Customer session — the path appears on an order owned by this customer
- Active checkout quote — the path appears on the customer’s current pre-checkout quote (guest uploads mid-checkout)
- Session-pinned upload — uploaded in this very session and not yet committed (instant preview)
First match wins. Any other path access returns HTTP 403.
7. Orphan cleanup
Section titled “7. Orphan cleanup”A daily cron job deletes files older than 7 days (configurable) that aren’t referenced by any quote or order row. Two side benefits:
- Disk space stays bounded
- Forgotten uploads don’t sit indefinitely as a latent data-exposure risk
Cron license heartbeat
Section titled “Cron license heartbeat”The license heartbeat (cda_customfields_license_heartbeat, daily at 03:30 UTC) sends:
- License key (encrypted at rest, decrypted only for this POST)
- Domain (cleaned: lowercase, no scheme, no port)
- Magento version + edition
- List of active CDA modules
…to https://magento.creativdigital.ro/api/heartbeat. No customer PII, no order data, no field values — only what’s necessary to verify your license is still valid and to surface domain-binding conflicts.
What’s NOT sent
Section titled “What’s NOT sent”- Customer records
- Order data
- Field definitions or values
- Anything from your DB beyond the four data points listed above
Network resilience
Section titled “Network resilience”The cron retains your previous status on:
- HTTP 5xx responses (server outage)
- Connection failures (DNS, firewall, network blip)
- Non-parseable JSON responses
Only explicit 2xx-4xx responses with a known status field downgrade you. A single licensing-server outage will never flip a healthy ACTIVE merchant into a locked state.
Audit reports
Section titled “Audit reports”The full security audit (run May 2026 ahead of v3.9.0) is committed at SECURITY_AUDIT.md in the magento2-custom-fields source repository. Available on request to enterprise customers — email office@creativdigital.ro.
Findings summary at time of audit:
- 4 High severity — all fixed in v3.9.0/v3.9.1
- 1 Medium severity — fixed
- 1 Medium accepted (guest-cart REST endpoint is anonymous, matching Magento’s standard guest-cart trust model)
- 0 Critical findings
Reporting a vulnerability
Section titled “Reporting a vulnerability”If you discover a security issue, please email office@creativdigital.ro with subject “Security disclosure” rather than posting it publicly. We’ll acknowledge within 24 hours, ship a patch within 1 week for confirmed issues, and credit you in the release notes if you’d like.