lead
Search "Magento 2 security hardening" and you get a dozen checklists that all say the same six things: patch, 2FA, SSL, file permissions, vet your extensions, back up. Every one of them is correct and none of them tells you the two things that actually decide whether the work happens — who on the team owns each item, and what it costs per month once it is live. This guide is the same layers with those two columns filled in — nine of them, because patch management gets its own rather than being a bullet, plus three places where the conventional advice is subtly wrong: 2FA is the wrong primary defence against the way admins actually get compromised, composer audit belongs in the pipeline rather than in a monthly calendar reminder, and file-upload validation done only in PHP is still bypassable on a default nginx config.
TL;DR — The highest-leverage change is not 2FA, it is SSO — putting Google Workspace or Microsoft Entra ID in front of the admin login so the admin password is never typed into a Magento form at all. 2FA protects you after a password is stolen; SSO removes the form the attacker is phishing for. Add 2FA second, as defence in depth, not first as the plan. The layers after it — REST API validation, data encryption, the file upload bypass — each carry an owner and a cost.
baseline: where a stock Magento 2 store actually stands
👥 For: Everyone (context, not implementation)
⏱ TL;DR: Default Magento 2 has known gaps in admin security, API validation, and secrets management. This guide fixes each.
A. Top 5 Real Attack Vectors (Seen in the Wild)
1. Admin Session Hijacking (25% of incidents)
- Attack: Unencrypted admin cookies over HTTP, or weak session timeout
- Impact: Full store control, data access, code deployment
- Fix: SSL enforcement, short session timeout (30 min), IP whitelist
2. REST API SearchCriteria Injection (20% of incidents)
- Attack: Bypass authorization filters via crafted SQL-like queries
- Example:
GET /V1/products?searchCriteria[filter_groups][0][filters][0][field]=sku&...value=* - Impact: Read arbitrary product data, customer data, pricing
- Fix: Whitelist allowed fields, validate filter depth
3. Weak File Upload Validation (15% of incidents)
- Attack: Upload .php disguised as image (e.g.,
shell.php.jpg) - Impact: Remote code execution, full server compromise
- Fix: Strict MIME type validation, rename files, store outside webroot
4. Plaintext Secrets in Code (15% of incidents)
- Attack: API keys, database credentials in
env.phpor code comments - Impact: Third-party service compromise, database breach
- Fix: Environment variables, AWS Secrets Manager, HashiCorp Vault
5. Admin Phishing (15% of incidents)
- Attack: Fake Magento login form tricks admin into typing password
- Impact: Same as session hijacking (full store access)
- Real data: 25% of admin breaches traced to phishing, not brute-force
- Fix: SSO (Google/Microsoft) so password never reaches fake form + 2FA as secondary
B. OWASP Top 10 (2021) Applied to Magento
| OWASP Risk | Magento Manifestation | Real Impact | Hardening Effort |
|---|---|---|---|
| Broken Access Control | Admin user roles not enforced in APIs | Unauthorized data access | 2–3 days |
| Cryptographic Failures | Passwords hashed with weak algorithm (MD5) | Offline cracking in minutes | 1–2 days |
| Injection | SearchCriteria + unvalidated input | SQL injection, RCE | 2–4 days |
| Insecure Design | No rate-limiting on checkout | Brute-force, credential stuffing | 1–2 days |
| Security Misconfiguration | Debug mode ON, error reporting verbose | Information disclosure | 2–4 hours |
| Vulnerable Components | Outdated PHP, MySQL versions | Known exploits apply directly | 1–4 weeks |
| Auth Failures | Weak password policy, no SSO/2FA, vulnerable to phishing | Credential compromise via phishing | 2–3 days (SSO) or 1–2 days (2FA only) |
| Software & Data Integrity | No code signing, auto-update disabled | Tampered code execution | 1–2 weeks |
| Logging & Monitoring Gaps | No audit trail of admin actions | Breach undetected for months | 2–3 days |
| SSRF | Unvalidated image proxy URLs | Internal service access | 1–2 days |
II. Layer 1: Admin Security
👥 For: Backend leads, DevOps
A. Implementation Owner: Who Hardens Admin Access
| Task | Owner | Effort | Why This Owner |
|---|---|---|---|
| SSO integration (Google/Microsoft) | Backend / DevOps | 2–3 days | OAuth provider setup, session mapping |
| 2FA rollout (secondary layer) | Backend / DevOps | 1–2 days | System access, auth integration |
| Session hardening (timeout, IP) | DevOps | 2–4 hours | Infrastructure + Magento config |
| SSL/TLS enforcement | DevOps | 2 hours | Certificate + web server config |
| IP whitelist for admin | DevOps | 1–2 hours | Firewall + .htaccess rules |
| Audit logging (admin actions) | Backend | 1–2 days | Database + observer setup |
B. SSO: Eliminate Phishing at the Source (PRIMARY)
Why SSO beats password-based auth:
PHISHING ATTACK FLOW:
❌ Password-based (vulnerable):
Attacker's email: "Click to verify admin access"
→ Admin clicks link
→ Lands on fake login form (looks like Magento)
→ Admin types username + password
→ Attacker captures credentials
→ Attacker logs in (2FA doesn't help if they have password + phone)
✓ SSO (Google/Microsoft) - immune to phishing:
Attacker's email: "Click to verify admin access"
→ Admin clicks link
→ Redirected to Google login (not Magento)
→ Admin sees: "This is from Google's domain" (not attacker's)
→ Admin recognizes phishing, closes tab
→ Even if admin types password, Google won't redirect back to fake site
→ Password never reaches Magento or attacker
Key difference: With password auth, phishing works if the fake form is convincing. With SSO, the password goes to Google/Microsoft, not Magento. Attacker can’t phish what they never see.
Magento SSO Options:
Option 1: Google Workspace (Enterprise)
├─ Setup: 2–3 hours (OAuth app, domain verification)
├─ Cost: €10–15/user/month (included in Workspace license)
├─ Benefit: Admin only logs in once (Google account)
└─ Security: Google's 2FA applies automatically
Option 2: Microsoft Entra ID (formerly Azure AD)
├─ Setup: 2–3 hours (OAuth app, tenant config)
├─ Cost: €4–6/user/month (Entra ID basic tier)
├─ Benefit: Windows shop integration (existing accounts)
└─ Security: Microsoft's 2FA, conditional access policies
Option 3: OpenID Connect (Any provider)
├─ Setup: 3–5 days (custom implementation)
├─ Cost: Provider-specific (Okta €2–4/user, Auth0 €20/month)
├─ Benefit: Centralized identity for multiple systems
└─ Security: Depends on provider's 2FA
Where SSO actually hooks in. Magento has no pluggable authentication-adapter interface — there is no AuthenticationAdapterInterface and nothing in Magento\Framework\Authentication to configure. Admin login runs through Magento\Backend\Model\Auth, which delegates credential checking to Magento\Backend\Model\Auth\Credential\StorageInterface, and Magento\User\Model\User dispatches admin_user_authenticate_before on the way through. Those three are the real seams: an observer on the event to enforce or short-circuit, a StorageInterface implementation to change what "valid credentials" means, and your own controller for the redirect and callback legs of the OAuth dance.
The reference implementation is in core rather than in a blog post: magento/module-admin-adobe-ims is a shipped, working admin SSO built on exactly these seams — it dispatches the same admin_user_authenticate_before event and swaps the credential storage. Read that module before writing a line of your own; a Google Workspace or Entra ID integration is the same shape with a different identity provider.
The part worth planning for is not the OAuth flow — libraries handle that — but the fallback: what happens when the identity provider is unreachable and nobody can reach the admin. Keep one break-glass local admin account with a long unique password and 2FA, excluded from the SSO path.
What happens after SSO login:
Admin flow:
1. Admin visits /admin
2. Not logged in → redirect to Google login
3. Admin enters Google credentials (or already logged in)
4. Google redirects back: /admin/auth/google/callback?code=xxx
5. Magento exchanges code for ID token
6. Magento creates session, admin sees dashboard
7. Admin logs out → Magento session destroyed (Google session persists)
Result: Admin never had to create Magento password.
Phishing doesn't work because password never goes through Magento's login form.
C. 2FA: TOTP-Based Authentication (SECONDARY)
Why 2FA still matters (as secondary layer): If SSO provider is compromised OR admin’s Google/Microsoft account is breached, 2FA adds protection.
Scenario: Attacker somehow got admin's Google password
├─ WITHOUT 2FA on Magento: Attacker can SSO into your store
├─ WITH 2FA on Magento: Even with Google access, attacker needs Magento 2FA code
└─ Defense: SSO prevents phishing, 2FA prevents compromise cascade
Enable as secondary check in System > Configuration > Admin > Security > Two-Factor Authentication
This creates defense-in-depth:
1. Primary: SSO (prevents phishing)
2. Secondary: 2FA (stops account compromise)
3. Tertiary: IP whitelist (stops VPN/datacenter abuse)
C. Session Hardening
// app/etc/env.php
'session' => [
'cookie' => [
'lifetime' => 1800, // ← 30 minutes
'secure' => 1, // ← HTTPS only
'httponly' => 1, // ← No JavaScript access
'samesite' => 'Strict', // ← CSRF protection
]
],
D. Admin IP Whitelist
For sensitive stores (high-value, payment processing):
# .htaccess (admin area)
<Files "index.php">
Order deny,allow
Deny from all
Allow from 203.0.113.0/24 # Your office
Allow from 198.51.100.0/24 # Remote team
</Files>
E. Weak Password Detection (Real-Time)
Enforce minimum 16 characters, uppercase, lowercase, digits, special characters.
III. Layer 2: REST API Security
👥 For: Backend, API developers
A. SearchCriteria Injection (Critical)
Vulnerability: Attackers can filter data they shouldn’t access via SearchCriteria params. The mechanics of how filter groups, conditions and sort orders compose are covered in the Magento 2 searchCriteria reference; what matters here is that the same expressiveness that makes the API useful turns an unwhitelisted endpoint into a data-export tool for anyone holding a low-privilege token.
Fix: Whitelist Allowed Fields
// app/code/Custom/Security/Plugin/ValidateSearchCriteria.php
private $allowedFields = [
'sku', 'name', 'status', 'visibility',
'created_at', 'updated_at',
];
B. Rate-Limiting on Login & Checkout
// 5 attempts per 15 min
$rateLimiter->checkLimit("login:$username", 5, 900);
C. API Token Rotation & Expiry
Tokens should expire after 24 hours. Force re-authentication.
IV. Layer 3: Data Encryption
👥 For: Backend, DevOps
Before touching database-level encryption, get Magento’s own encrypted-config surface right — a custom system.xml field missing type="obscure" leaks its value in Adminhtml and the next save can corrupt it, both described in the key rotation trap. That is a bug you ship yourself; TDE below is infrastructure.
A. Database Encryption (TDE)
ALTER DATABASE magento2 ENCRYPTION='Y';
B. PII Masking (Customer Data at Rest)
Mask in logs/exports, not in user-facing data.
C. Secrets Management
WRONG: API keys in code
RIGHT: Environment variables or AWS Secrets Manager
working_example: Layer 4 — the file upload bypass that still works
👥 For: Frontend, Backend, DevOps
The Problem: Magento 2 allows customer file uploads in many places:
├─ Product reviews (customer can attach images)
├─ Warranty claims (PDF documentation)
├─ Custom product uploads (print-on-demand, engraving)
├─ Support tickets (customer screenshots, invoices)
└─ Account data (profile pictures, certificates)
If any of these lack validation, attacker uploads shell.php.jpg → RCE → full server compromise.
A. Real Magento 2 Upload Vulnerabilities
Vulnerability 1: MIME Type Spoofing
Attacker uploads: shell.php.jpg
├─ File extension: .jpg (looks safe)
├─ File content: <?php system($_GET[0]); ?>
├─ Magic bytes: FF D8 FF (JPEG header, but payload is PHP)
├─ Server sees: "It has .jpg extension, must be safe" ✓ WRONG
└─ Result: PHP executes, server compromised
Vulnerability 2: Polyglot Files
Attacker creates: valid.jpg + embedded PHP
├─ File is 100% valid JPEG (renders as image)
├─ Appended PHP code: <?php system($_GET[0]); ?>
├─ Server sees: "It renders as image, must be safe" ✓ WRONG
├─ Nginx misconfigured: executes .jpg as PHP
└─ Result: Image displays fine, but PHP runs if accessed via PHP handler
Vulnerability 3: Null Byte Injection (older PHP)
Attacker uploads: shell.php%00.jpg
├─ Server strips null byte: shell.php
├─ Extension validation sees: shell.jpg ✓ looks safe
├─ But actual file created: shell.php
└─ Result: Executes as PHP (patched in PHP 5.3+, but watch for legacy)
Vulnerability 4: Double Extension
Attacker uploads: shell.php.jpg
├─ Nginx misconfigured: processes .php files in upload dir
├─ Nginx sees: *.php* (matches both rules if misconfigured)
├─ Extension validation sees: shell.jpg ✓ looks safe
├─ But: Nginx executes it as .php anyway
└─ Result: PHP executes
B. Server-Level Prevention (Nginx Configuration)
CRITICAL: Uploads directory must NEVER execute PHP
# /etc/nginx/conf.d/magento-uploads.conf
# Block PHP execution in upload directories
location ~ /media {
# Explicitly block PHP execution
location ~ \.php$ {
deny all;
}
# Allow only safe file types
location ~ \.(jpg|jpeg|png|gif|pdf|txt|doc|docx|zip)$ {
allow all;
}
# Block everything else
location ~ {
deny all;
}
}
# Same for var/import, var/export, var/tmp
location ~ /(var/import|var/export|var/tmp) {
location ~ \.php$ {
deny all;
}
}
# .htaccess files: should not be served
location ~ /\.ht {
deny all;
}
What this does:
├─ Attacker uploads shell.php.jpg to /media/
├─ Attacker tries to access: /media/shell.php.jpg
├─ Nginx blocks it (not in allowed file types)
├─ If renamed to shell.php: Nginx blocks (explicit .php block)
└─ Result: Can't execute, only store as binary data
C. Application-Level Validation (3-layer check)
Layer 1: File Extension Whitelist
// app/code/Custom/Security/FileUploadValidator.php
private $allowedExtensions = [
'jpg', 'jpeg', 'png', 'gif', // Images
'pdf', 'txt', 'doc', 'docx', // Documents
];
public function validate($filename) {
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
// DANGER: Never trust extension alone
if (!in_array($extension, $this->allowedExtensions)) {
throw new Exception("File type not allowed: $extension");
}
}
Layer 2: MIME Type Verification (fileinfo)
public function validateMimeType($filePath) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
$allowedMimes = [
'image/jpeg' => ['jpg', 'jpeg'],
'image/png' => ['png'],
'image/gif' => ['gif'],
'application/pdf' => ['pdf'],
];
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
// MIME type must match extension
if (!isset($allowedMimes[$mimeType]) ||
!in_array($extension, $allowedMimes[$mimeType])) {
throw new Exception("MIME type mismatch or not allowed: $mimeType");
}
}
Layer 3: Magic Bytes Verification
public function validateMagicBytes($filePath, $extension) {
$fh = fopen($filePath, 'rb');
$magicBytes = fread($fh, 12);
fclose($fh);
// Check actual file signature (magic bytes)
$signatures = [
'jpg' => [0xFF, 0xD8, 0xFF], // JPEG: FF D8 FF
'png' => [0x89, 0x50, 0x4E, 0x47], // PNG: 89 50 4E 47
'gif' => [0x47, 0x49, 0x46], // GIF: 47 49 46
'pdf' => [0x25, 0x50, 0x44, 0x46], // PDF: %PDF
];
if (!isset($signatures[$extension])) {
throw new Exception("No signature validation for: $extension");
}
$expected = $signatures[$extension];
$actual = array_slice(unpack('C*', $magicBytes), 0, count($expected));
if ($actual !== $expected) {
throw new Exception("File signature does not match extension");
}
}
Layer 4: Size Limit
// Prevent zip bomb / decompression attacks
private $maxFileSize = 10 * 1024 * 1024; // 10 MB
if (filesize($filePath) > $this->maxFileSize) {
throw new Exception("File too large");
}
D. Store Uploads Outside Webroot
Good practice:
Webroot: /var/www/html/
Uploads: /var/uploads/ (NOT under webroot)
When file accessed:
/media/uploads/12345.jpg
→ Nginx: resolve to /var/uploads/12345.jpg (not in webroot)
→ Served as static file, never executed
→ If somehow PHP: won't execute (outside web dirs)
E. Rename Uploaded Files (Remove Original Metadata)
CRITICAL: Never keep original filename
public function saveFile($uploadedFile) {
// Original: warranty-claim-shell.php.jpg
// Store as: a3f2b1c9d8e7f6g5.jpg
$newName = bin2hex(random_bytes(16)) . '.' . $extension;
$path = '/var/uploads/' . $newName;
// Original filename stored in database if needed for UI
$this->db->insert('uploaded_files', [
'stored_filename' => $newName,
'original_filename' => $uploadedFile->getClientOriginalName(),
'mime_type' => $mimeType,
'size' => filesize($filePath),
'uploader_id' => $customerId,
'uploaded_at' => now(),
]);
}
Why renaming matters:
Attacker uploads: shell.php.jpg
├─ Original name exposed in web: /media/shell.php.jpg
├─ Attacker guesses path and executes it
├─ If renamed: /media/a3f2b1c9d8e7f6g5.jpg
├─ No .php extension visible
├─ Nginx rules block .jpg from PHP execution
└─ Attacker can't find or execute it
F. Real-World Scenario: Magento 2 Review Image Upload
Customer leaves 5-star review + attaches image:
1. Frontend form: POST /reviews/product/12345
2. File: product-review.php.jpg (size: 5KB)
3. MIME check: Claims "image/jpeg"
4. Magic bytes: Actually starts with FF D8 FF (valid JPEG header)
5. Hidden payload: PHP code appended after image
Validation fails if:
├─ Only checks extension (.jpg looks safe)
├─ Only checks MIME type (can be spoofed)
├─ Only checks magic bytes (appended code after JPEG data)
└─ File stored in /media/ with .php execution enabled
Validation passes if ALL 4 checks done:
├─ Extension: jpg ✓
├─ MIME type: image/jpeg ✓
├─ Magic bytes: FF D8 FF ✓
├─ Size: reasonable (not bomb) ✓
└─ Nginx: blocks .php in /media/ ✓
└─ Name: renamed, no extension visible ✓
G. Implementation Checklist
MUST HAVE (blocking production):
├─ [ ] Nginx: Explicit .php block in /media/, /var/import, /var/export
├─ [ ] App: MIME type validation (fileinfo)
├─ [ ] App: Magic bytes verification
├─ [ ] File: Renamed to random hash (no original name)
├─ [ ] Storage: Outside webroot if possible
└─ [ ] Size: Limit enforced (prevent zip bombs)
SHOULD HAVE:
├─ [ ] Scan for embedded payloads (Sansec does this)
├─ [ ] Quarantine uploads for 24h before publishing
├─ [ ] Audit trail: who uploaded what, when
└─ [ ] Rate-limit uploads (prevent spam/bomb)
OPTIONAL:
├─ [ ] Virus scan (ClamAV integration)
├─ [ ] Image re-compression (strip metadata/EXIF)
└─ [ ] Whitelist by customer group (some users can upload, others can't)
VI. Layer 5: Adobe security patches (the layer everyone lists and nobody schedules)
👥 For: Backend, DevOps, and whoever owns the release calendar
Composer dependencies are the next section. This one is about Magento itself, and it is separated deliberately: composer audit will not tell you that Adobe published an advisory, because Adobe security releases ship as new patch versions and quarterly security-only patches, not as advisories against your existing lockfile.
A. Ownership and cadence
| Item | Owner | Cadence | Target |
|---|---|---|---|
| Watch the advisory feed | Backend lead | Continuous (subscription, not polling) | Same-day awareness |
| Triage a new advisory against this store | Backend lead | On publication | Within 24 h |
| Apply a critical patch | Backend + DevOps | On triage | 72 hours |
| Apply a routine security release | Backend + DevOps | Scheduled | 30 days |
| Verify the patch landed in production | DevOps | Per deployment | Same release |
The 72-hour and 30-day figures are the numbers most Magento security guidance converges on, and they are worth writing into an actual agreement rather than leaving as intent. Adobe advisories are public the moment they publish, which means the window between publication and mass exploitation is measured in days — attackers read the same feed you do, and they read it more reliably.
B. Subscribe, do not poll
Adobe Commerce security bulletins → https://helpx.adobe.com/security/products/magento.html
Adobe PSIRT mailing list → subscribe with a shared team address, not one person's
Sansec advisories → independent second source, often earlier
Route these to a channel the team actually reads. A bulletin that arrives in one engineer’s personal inbox while they are on holiday is not a control.
C. Triage: does this advisory apply to us?
Not every advisory needs a 72-hour scramble, and treating them all as critical is how teams end up ignoring all of them. Three questions, in order:
1. Is the affected component installed and enabled here? bin/magento module:status plus the version in composer.lock. B2B and Commerce-only modules account for a large share of advisories that do not apply to an Open Source store.
2. Is the attack path reachable? An admin-authenticated RCE in a store behind SSO and an IP allowlist (Layer 1) is a different urgency from a pre-auth RCE on a public endpoint.
3. Is there compensating control? A WAF with a rule already deployed buys calm hours — that is the concrete value of the tooling in the tradeoff section, not a nice-to-have.
Record the answer even when it is "does not apply". The audit question six months later is not "did you patch" but "how did you decide", and an empty record reads identically to negligence.
D. Applying the patch without a weekend
# Composer-managed security release (preferred)
ddev composer require magento/product-community-edition=2.4.8-p2 --no-update
ddev composer update magento/product-community-edition --with-dependencies
# Individual patch file (cweagans/composer-patches), when a full version bump is not viable
ddev composer require cweagans/composer-patches
# add the patch to composer.json "extra.patches", then:
ddev composer install
ddev exec php bin/magento setup:upgrade
ddev exec php bin/magento cache:flush
ddev exec php bin/magento setup:di:compile
Staging first, always, with the same four smoke paths every time: admin login, catalog search, add to cart, checkout through to order placement. A security patch that breaks checkout has cost you more than the vulnerability would have.
E. When you cannot patch in time
Sometimes the version bump requires a PHP upgrade, or a third-party module has not certified compatibility yet, and the honest answer is "not this week". That is a real situation and it has a real response, which is not "hope":
- Deploy a WAF rule or virtual patch covering the specific attack path.
- Disable the affected module if it is not business-critical.
- Restrict the vulnerable endpoint at the web-server layer (IP allowlist, or a hard
deny). - Raise monitoring on that path specifically, so exploitation is noticed rather than discovered.
- Put a date on the real fix and treat the mitigation as debt, not resolution.
Every item on that list assumes a patch exists and you simply cannot apply it yet. The harder case is the one where there is nothing to apply: Magento Attack Surface: Don’t Ship Code You Don’t Need works through StyleSmuggler (CVE-2026-75650), where fully patched 2.4.4-2.4.9 stores were being compromised for three days before Adobe shipped a hotfix. During that window the only defences that held were ones the store had bought in advance – and the one that held best was simply not having shipped the vulnerable subsystem at all.
VII. Layer 6: Dependency Security (Composer Audit)
👥 For: Backend, DevOps, Release Manager
⏱ Effort: 1–2 hours setup, 30 min weekly maintenance
A. The Risk: Known Vulnerabilities in Dependencies
Magento stores run 100–200+ Composer dependencies. Each dependency can have known security vulnerabilities. A single vulnerable package = full store compromise.
Real scenario:
Your store uses: vendor/somevendor/vulnerable-plugin v1.2.3
├─ This version has known RCE vulnerability (CVE-2024-1234)
├─ Attacker exploits via REST API
├─ Server compromised before you even know about it
└─ Cost: €50k+ in remediation
B. Check Current Vulnerabilities
Command: Identify known vulnerabilities
# Run from project root
ddev composer audit
# Output shows:
Found 3 vulnerabilities:
1. vendor/package1 (v1.0.0)
└─ RCE via SearchCriteria injection (CVE-2024-1234)
└─ Severity: CRITICAL
└─ Fixed in: v1.0.1
2. vendor/package2 (v2.1.0)
└─ SQL injection in export filter (CVE-2024-1235)
└─ Severity: HIGH
└─ Fixed in: v2.2.0
3. vendor/package3 (v1.5.0)
└─ Information disclosure (CVE-2024-1236)
└─ Severity: MEDIUM
└─ Fixed in: v1.5.1
What each severity means:
- CRITICAL: Immediate RCE or data breach risk. Update within 24 hours.
- HIGH: Authentication bypass, SQL injection, etc. Update within 1 week.
- MEDIUM: Information disclosure, weak crypto. Update within 2 weeks.
- LOW: DoS, weak logging, etc. Update before next release.
C. Automated Checks in CI/CD
Add to your deployment pipeline:
# .gitlab-ci.yml or .github/workflows/security.yml
security_audit:
stage: build
script:
- ddev composer audit --format json > audit.json
# Fail if CRITICAL or HIGH found
- |
if grep -q '"severity":"CRITICAL\|\"severity":"HIGH' audit.json; then
echo "❌ Security vulnerabilities found. Fix before merging."
exit 1
fi
artifacts:
reports:
dependency_scanning: audit.json
What this does:
- Blocks merge requests with CRITICAL/HIGH vulnerabilities
- Forces developers to update dependencies before deploying
- Tracks vulnerability history in CI dashboard
D. Safe Update Process (Zero-Downtime)
Step 1: Check what’s outdated
ddev composer outdated
# Shows all packages with available updates
Step 2: Update specific package (or all)
# Update ONLY the vulnerable package (safest)
ddev composer update vendor/package1 --no-dev
# OR update all minor versions (recommended monthly)
ddev composer update --no-dev --dry-run
# Review changes, then:
ddev composer update --no-dev
Step 3: Test before deploying
# Run on staging first
ddev exec php bin/magento setup:upgrade
ddev exec php bin/magento setup:di:compile
ddev exec php bin/magento cache:flush
# Test critical flows:
# - Admin login
# - Product search
# - Add to cart / checkout
# - Customer account creation
Step 4: Deploy to production
# Tag release with version bump
git tag v2.4.6-security-patch-1
git push origin main --tags
# Deploy (or trigger CD pipeline)
DRY_RUN=0 bin/sync-to-prod.sh
E. Monthly Security Audit Routine
Every month, run this checklist:
# 1. Check for new vulnerabilities
ddev composer audit
# 2. Check for outdated packages
ddev composer outdated
# 3. Check for deprecated packages
ddev composer show --outdated --direct
# 4. Check if Composer lockfile is up-to-date
ddev composer update --dry-run
# 5. Check PHP version support (Magento requires PHP 7.4+)
php -v
# 6. Check MySQL/MariaDB for EOL versions
ddev mysql -e "SELECT VERSION();"
# 7. Check for known Magento CVEs (subscribe to security list)
# https://magento.com/security/updates
Recommended schedule:
- Weekly: Automated CI/CD checks (runs on every commit)
- Monthly: Manual audit + outdated review (30 min)
- Quarterly: Major version updates (if applicable)
- On notification: Immediate patching for CRITICAL vulns
F. Example: Updating a Vulnerable Package
Real scenario: PHP XML-RPC vulnerability found
# 1. Check what's vulnerable
ddev composer audit
# Output:
# vendor/symfony/xml-rpc v1.2.0
# └─ XXE vulnerability (CVE-2024-1234)
# └─ Fixed in: v1.3.0
# 2. Update just that package
ddev composer update symfony/xml-rpc
# 3. Verify update
ddev composer show symfony/xml-rpc
# Shows: v1.3.0 (was v1.2.0) ✓
# 4. Commit
git add composer.json composer.lock
git commit -m "security: update symfony/xml-rpc to v1.3.0 (CVE-2024-1234)"
# 5. Push to staging, test, deploy
git push origin develop
# (CI/CD runs tests, deploys to staging)
# (Manual testing)
# (Deploy to production)
G. What to Watch For (Common Misses)
❌ Not updating Composer itself
# Composer has security updates too!
composer self-update
❌ Only checking direct dependencies
- Magento > Framework > Component > Vendor
- Vulnerability in 5-levels-deep dependency still exploitable
- Use
composer audit(checks full tree)
❌ Pinning old versions to "avoid breaking changes"
// BAD: "magento/framework": "2.4.0" (freezes, misses security fixes)
// GOOD: "magento/framework": "^2.4.0" (gets patches, not breaking changes)
❌ Ignoring medium-severity vulns
- "It’s just information disclosure" → can leak API keys
- "DoS only" → still takes site down
H. Vendor Lock-In Warning (First-Party Packages)
When updating first-party packages (e.g., vendor/brocode/*):
# 1. Check if package uses git source
ddev composer show vendor/brocode/module-image-optimizer-webp
# Output: type: library, source: git (https://github.com/brosenberger/...)
# = You're using the live repo, not a release
# 2. Verify correct branch is checked out
cd vendor/brocode/module-image-optimizer-webp && git branch
# Should show: main or release branch, not detached
# 3. Pull latest from remote
cd vendor/brocode/module-image-optimizer-webp && git pull origin main
# 4. Return to project root and reindex
cd ../../../
ddev composer update --lock
VIII. Layer 7: GDPR & Data Privacy
👥 For: Legal, Backend, Data Protection Officer
A. Right to Be Forgotten (Data Deletion)
Customers can request deletion of all personal data. Anonymize remaining records.
B. Data Export (Right to Portability)
Customers can export their data in portable format (JSON/CSV).
IX. Layer 8: Incident Response Plan
👥 For: Leadership, Backend, DevOps, Communications
A. Incident Response Timeline (First 24 Hours)
T+0 min └─ Incident detected → Activate incident commander
T+15 min └─ Isolate compromised account → Preserve logs
T+1 hour └─ Notify external parties → Deploy patch
T+24 hours └─ Customer notification complete
The runbook itself, with six worked scenarios and real timings, lives in the companion Magento 2 incident recovery guide — including the "we’ve been hacked" path that starts where this layer ends. An audit trail is only useful if one request can be followed end to end, which is what request-scoped log tracing exists for.
B. Incident Runbook Template
- Detection
- Immediate Response (disable credentials, snapshot DB)
- Investigation (scope, attack vector)
- Remediation (patch, test, deploy)
- Communication (customer notification, regulators)
- Long-term (root cause analysis, training)
tradeoff: Layer 9 — breach detection and prevention, and what it costs
👥 For: DevOps, Security, Leadership
Correction, 2026-09-30: an earlier version of this section described Sansec Shield as a DNS-level edge proxy with brute-force rate limiting and bot filtering, and quoted monthly prices without a source. Shield is a Composer module that runs inside Magento, Sansec documents no rate-limiting or bot features for it, and its plans are sold by store-revenue tier. The section below replaces it.
A. Sansec eComscan (malware and vulnerability detection)
What it does: eComscan scans the store’s files and database for malware, known-vulnerable extensions and unauthorized admin accounts, with Sansec’s threat intelligence behind the signatures. It detects; it does not block. That makes it the tool that answers "are we already compromised?" — the question the breach path in the incident recovery runbook starts with.
PCI DSS: PCI DSS v4.0 asks for anti-malware and change detection, not for a particular vendor. A scanner report is useful evidence in an assessment; it is not a requirement on its own.
Plan: included from Sansec’s entry plan (Secure) upwards.
B. Sansec Shield (in-application WAF)
What it is: a Magento module, sansec/magento2-module-shield, that runs inside the application and blocks requests matching known Magento attack patterns. Sansec’s researchers publish new rules as they see attacks, and the module syncs them without a deploy.
ddev composer require sansec/magento2-module-shield
ddev exec bin/magento setup:upgrade
ddev exec bin/magento config:set sansec_shield/general/license_key YOURKEY
ddev exec bin/magento sansec:shield:sync-rules
What it is not:
- Not an edge proxy. No DNS change, no traffic routed through Sansec. Every request it evaluates has already passed your web server, Varnish and PHP bootstrap — which is also why it cannot be bypassed by hitting the origin address directly, the classic weakness of CDN WAFs.
- Not rate limiting, brute-force protection or bot filtering. Sansec documents none of those for Shield. Login and checkout rate limiting is covered under Layer 2 above; bot load belongs at the proxy in front of Varnish, covered in Magento bot protection at the proxy.
- Not a replacement for patching. It covers the window between disclosure and your deployment, and only for attacks Sansec has written a rule for.
The case for it, with real dates — PolyShell (CVE-2026-48356):
2026-03-16 Shield starts blocking exploitation attempts
2026-03-17 Sansec publishes the warning: unauthenticated file upload via the guest-cart REST API
2026-03-19 First attacks in the wild; mass scanning follows
2026-05-12 Adobe ships a production fix
2026-07-14 Backports to 2.4.8 / 2.4.7 and older lines (APSB26-73)
For older release lines that is four months between public exploit and an official patch. Sansec reports Shield blocked PolyShell attempts against 79.5% of the stores it protects by late March. That window — not a generic "WAF blocks SQL injection" table — is what Shield is for: it turns an emergency deploy into a scheduled one.
Plan: included in Sansec’s Advanced and Enterprise plans, not in the entry plan.
C. When to use each
| Scenario | eComscan | Shield | Why |
|---|---|---|---|
| After a suspected breach | ✓ | ✓ | eComscan finds what happened; Shield covers the known attack paths while you clean up |
| Before a major deployment | ✓ | — | Confirms nothing malicious is left behind |
| PCI DSS assessment | ✓ | optional | Scanner reports are evidence for the malware and change-detection requirements |
| Stores taking card data on-site | ✓ | ✓ | Detect plus prevent |
| Development-stage store | — | — | Not yet worth the cost |
| Patch cycle slower than a few days | ✓ | ✓ | Shield exists for exactly the gap between disclosure and deploy |
| Budget tight | ✓ | — | Detection first; Shield when the patch cadence cannot keep up |
verification: the implementation checklist
Quick Wins (< 1 day each)
- [ ] Subscribe the team channel to Adobe security bulletins (not one person’s inbox)
- [ ] Check your current patch level against the latest security release
- [ ] Audit dependencies:
ddev composer audit(find known vulnerabilities) - [ ] Set admin session timeout to 30 min
- [ ] Enable HTTPS for admin area
- [ ] Set secure/httponly cookie flags
- [ ] Whitelist admin IP addresses
- [ ] Set up Sansec eComscan in monitoring mode
Medium Effort (1–3 days each)
- [ ] Write down the patch SLA — 72 h for critical, 30 days for routine — and name the owner
- [ ] Define the four smoke paths run after every security patch on staging
- [ ] Implement SSO (Google Workspace or Microsoft Entra ID) – PRIMARY anti-phishing defense
- [ ] Setup CI/CD dependency checks: Add
ddev composer auditto deployment pipeline (blocks CRITICAL/HIGH vulns) - [ ] Update all vulnerable packages: Fix issues found by
composer audit(Section V) - [ ] Enable 2FA for all admin users (secondary layer, after SSO)
- [ ] Implement SearchCriteria whitelisting
- [ ] Add rate-limiting to login
- [ ] Validate file uploads (MIME + magic bytes)
- [ ] Implement secrets management
- [ ] Add audit logging for admin actions
- [ ] Create GDPR deletion workflow
- [ ] Install Sansec Shield (Composer module, Advanced plan or higher)
Major Work (1–2 weeks)
- [ ] Enable database encryption (TDE)
- [ ] Implement incident response plan (see Incident Recovery article)
- [ ] Audit all REST API endpoints
- [ ] Security training for team
- [ ] Penetration test (external + use Sansec findings)
- [ ] Compliance audit (PCI DSS assessment with Sansec report)
Ongoing Maintenance (Monthly)
- [ ] Triage every advisory published since last month and record the decision, including "does not apply"
- [ ] Run
ddev composer audit– Check for new vulnerabilities in dependencies - [ ] Run
ddev composer outdated– Identify packages with available updates - [ ] Review Magento security bulletins – Subscribe to https://magento.com/security/updates
- [ ] Update CRITICAL and HIGH severity packages immediately (within 24-48h)
- [ ] Update MEDIUM severity packages within 2 weeks
- [ ] Test dependency updates on staging before production deployment
Expected timeline: 4–6 weeks for full hardening.
Tooling cost:
| Layer | Tool | Sansec plan | Purpose |
|---|---|---|---|
| Detection | eComscan | Secure (entry) and up | Find breaches and vulnerable extensions |
| Prevention | Shield | Advanced and up | Block known Magento exploits until you patch |
Sansec sells its plans by annual store revenue tier (up to €3m, €20m and €50m); check sansec.io/pricing for current figures.
Related reading
- Magento 2 Incident Recovery: 6 Scenarios and a Runbook — what to do in the first five minutes when a layer here fails, including the breach path.
- Magento Attack Surface: Don’t Ship Code You Don’t Need — the argument for removing what you do not use, made from a 0-day that beat every patched store.
- Magento 2 searchCriteria: Complete REST Reference — the filter grammar Layer 2 whitelists against.
- Magento 2 REST API Gaps — where the API surface stops, and what teams bolt on to fill it.
- Magento 2 Encrypted Config: The Key Rotation Trap — the
system.xmlmistake that leaks secrets without any attacker involved. - Magento 2 Request Tracing: One ID, Every Log Line — audit trails you can actually follow across a request.
- MCP Security for Ecommerce — if an LLM holds a token against your store, Layer 2 is not enough on its own.
Sources & References
- Adobe Commerce security bulletins — the authoritative CVE feed; subscribe rather than poll.
- Adobe Commerce security best practices — Adobe’s own baseline, which this guide assumes as already done.
- OWASP Top 10 — the risk taxonomy mapped to Magento in the baseline section.
- Composer
auditcommand — the dependency check Layer 5 wires into CI. - Sansec: eComscan, Shield, pricing and the PolyShell research behind the timeline in Layer 9.
- PCI DSS v4.0 — the compliance frame behind the encryption and logging layers.
Disclaimer
Costs in this article are list prices at the time of writing and vary by store size and region. Effort estimates assume a team already familiar with the codebase — a first-time hardening pass on an unfamiliar store runs longer. Nothing here replaces a penetration test or a formal PCI assessment; it is the work that makes those cheaper.