| Version | Supported |
|---|---|
| 1.x | β Yes (current) |
| < 1.0 | β No |
Please do NOT report security vulnerabilities through public GitHub Issues.
If you discover a security vulnerability in this project, please report it privately:
- Email: open a GitHub Security Advisory
- Or contact the author directly via GitHub: @fabconejo
- Description: What is the vulnerability? What can an attacker do?
- Affected versions: Which version(s) are impacted?
- Reproduction steps: How can we reproduce it?
- Impact assessment: What data/systems could be affected?
- Suggested fix (optional but appreciated!)
- Acknowledgment: Within 48 hours
- Initial assessment: Within 5 business days
- Fix + disclosure: Within 30 days (we follow responsible disclosure)
- Never log API keys or secrets β the bundle deliberately excludes credentials from all log output
- Environment variables only β credentials are never hardcoded; they must come from
.env - Validation at construction β empty credentials throw immediately, preventing silent failures
// β
CORRECT β use environment variables
api_key: '%env(MAILJET_API_KEY)%'
// β NEVER DO THIS β hardcoded in config
api_key: 'my_real_api_key_123'All incoming webhook requests from Mailjet are verified using HMAC-SHA256 with a shared secret:
// MailjetClient uses hash_equals() for timing-safe comparison
// This prevents timing attacks that could leak the secret
$expectedSignature = hash_hmac('sha256', $requestBody, $webhookSecret);
return hash_equals($expectedSignature, $receivedSignature);Why hash_equals() instead of ===?
A regular string comparison (===) takes different amounts of time depending on where the strings differ. An attacker could measure these tiny time differences to guess the secret character by character (this is called a timing attack). hash_equals() always takes the same amount of time, regardless of how different the strings are.
The bundle deliberately does NOT log:
- Email bodies (could contain personal/sensitive data β GDPR!)
- Attachment contents
- Custom variables or headers (could contain tokens, PII)
What IS logged:
- Recipient count
- Subject line (note: keep subjects non-sensitive)
- API response status
- Error messages (sanitized)
- Email format validation: Every
EmailAddressobject validates format on creation using PHP's built-infilter_var()withFILTER_VALIDATE_EMAIL - Attachment size limit: Files over 15 MB are rejected before the API call (Mailjet's limit)
- Batch size limit: Batches over 50 messages are rejected with a clear error
- Priority bounds: Email priority must be 1β5
EmailAddress is a readonly class in PHP 8.2+. Once created, its properties cannot be changed. This prevents bugs where code accidentally mutates an email address mid-flow.
Enable sandbox mode to validate email structure without sending real emails:
# config/packages/mailjet.yaml (dev environment)
mailjet:
sandbox_mode: trueThis sends the request to Mailjet but tells their API to validate and respond without actually delivering the email. No risk of accidentally emailing your customers from a dev server!
Before going live, verify:
-
.envis listed in.gitignoreβ NEVER commit it -
MAILJET_API_KEYandMAILJET_SECRET_KEYare set in production environment variables -
MAILJET_SANDBOX_MODE=falsein production -
MAILJET_WEBHOOK_SECRETis set to a random 32+ character string- Generate one:
php -r "echo bin2hex(random_bytes(32));"
- Generate one:
- Webhook URL (
/mailjet/webhook) is protected by HTTPS only - Consider adding rate limiting to the webhook endpoint (e.g. Symfony RateLimiter)
- Review Mailjet's IP allowlist to restrict webhook origins
- Mailjet Security Overview
- OWASP API Security Top 10
- PHP Security Guide
- Symfony Security Best Practices
This security policy was created alongside the initial release. It will be updated as the project evolves.
Author: Fabien ConΓ©jΓ©ro β Last updated: February 20, 2026