Skip to content

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.

CDA is designed to resist:

ThreatMitigation
SQL injection on field valuesParameterized queries throughout; no string interpolation into WHERE/SELECT
XSS in admin / storefront / email renderingEvery output passes through Magento’s Escaper (escapeHtml, escapeUrl, escapeHtmlAttr)
CSRF on admin savesMagento’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 pathsStorage paths are server-generated (yyyy/mm/<32-hex>.<ext>) and never derived from user input. Strict regex validation on retrieval.
Unauthorized file downloadsHMAC-SHA256 signed URLs + 4-tier ownership ladder (admin / order owner / current quote / session-pinned)
Mass assignment via inline admin gridWhitelist of grid-editable columns; non-whitelisted POST keys silently dropped
Privilege escalation via wrong ACLEach 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 leakLicense key stored encrypted via Magento\Framework\Encryption\EncryptorInterface (uses installation crypt key)

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

The file upload flow is the most sensitive surface. Each step is hardened:

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).

$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $tmpPath); // reads magic bytes

We 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/jpegjpg, etc.) — so a .exe renamed to .jpg becomes a .bin rejected.

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

Default 10MB per file. Configurable per store. Magento’s upload_max_filesize / post_max_size PHP limits still apply as outer envelope.

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).

A valid HMAC signature alone is NOT sufficient. The Download controller layers an authorization check:

  1. Admin session — full access (admins see every order’s attachments)
  2. Customer session — the path appears on an order owned by this customer
  3. Active checkout quote — the path appears on the customer’s current pre-checkout quote (guest uploads mid-checkout)
  4. Session-pinned upload — uploaded in this very session and not yet committed (instant preview)

First match wins. Any other path access returns HTTP 403.

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

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.

  • Customer records
  • Order data
  • Field definitions or values
  • Anything from your DB beyond the four data points listed above

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.

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

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.