GDPR Compliance Analyzer
| Analyzer ID | Category | Severity | Time To Fix |
|---|---|---|---|
gdpr-compliance | 🛡️ Security | Medium | 30 minutes |
What This Checks
Scans app/Models/User.php, app/Models/Profile.php, app/Models/Customer.php, database/migrations/, routes/web.php, routes/api.php, and config/logging.php for technical GDPR compliance signals. Checks for:
- Data deletion capability (Article 17) -
SoftDeletestrait, anonymize/purge method on the User model, or a dedicatedUserDeletionService/UserErasureService - Encryption at rest -
encryptedcasts inUser,Profile, andCustomermodels for sensitive personal fields that are actually present and not already encrypted (e.g.phone,address,national_id). Identifier fields such asemail,password, andremember_tokenare intentionally excluded, and a model with no encryptable sensitive field (e.g. a thin auth model with onlyname,email,password) is not flagged. - Privacy policy route - a
/privacyor/privacy-policyroute in web or API route files - Data export capability (Article 20) - an export controller, export route,
app/Exports/directory, orexport/downloadDatamethod on the User model - PII protection in logs - log sanitization (
SanitizeLog,mask,redact,scrub,replace_placeholders) inconfig/logging.phpor a custom processor inapp/Logging/
SoftDeletes is not sufficient for true erasure
SoftDeletes preserves all personal data in the database: soft-deleted records are still readable by database admins and analytical tools. GDPR Article 17 requires that data be truly erased or anonymized. Add an anonymize() method alongside SoftDeletes, or use the Prunable / MassPrunable traits to schedule automatic hard-deletion of soft-deleted records after a retention period.
Why It Matters
- Legal Obligation: GDPR fines can reach 4% of annual global turnover or EUR 20 million
- Right to Erasure (Art. 17): Users must be able to request deletion or anonymization of their personal data
- Data Protection (Art. 25): Sensitive personal data must be encrypted at rest
- Lawful Basis (Art. 6): All processing requires a documented lawful basis — typically contractual necessity for SaaS core features, or consent for marketing and non-essential processing
- Transparency (Art. 13/14): Users must be informed about how their data is processed via a privacy policy
How to Fix
Quick Fix (10 minutes)
Add a privacy policy route and data deletion capability:
// routes/web.php
Route::get('/privacy-policy', [PageController::class, 'privacy'])
->name('privacy-policy');
// app/Models/User.php
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Authenticatable
{
use SoftDeletes;
public function anonymize(): void
{
$this->update([
'name' => 'Deleted User',
'email' => 'deleted-' . $this->id . '@anonymized.local',
]);
$this->delete();
}
}Proper Fix (30 minutes)
1. Encrypt sensitive personal data:
class User extends Authenticatable
{
protected $casts = [
'phone' => 'encrypted',
'address' => 'encrypted',
'date_of_birth' => 'encrypted:date',
];
}Do not encrypt login or unique identifier fields
Laravel's encrypted cast is non-deterministic — it uses a random IV per write, so the same plaintext encrypts to a different ciphertext each time. Applying it to email (or any field you query by equality or enforce a unique constraint on) breaks where('email', ...) lookups, authentication, and the unique index. These fields need deterministic or blind-index encryption, which Laravel does not ship out of the box. The analyzer never recommends encrypting email, password, or remember_token for this reason.
2. Implement true erasure alongside SoftDeletes:
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Authenticatable
{
use SoftDeletes;
public function anonymize(): void
{
$this->update([
'name' => 'Deleted User',
'email' => 'deleted-' . $this->id . '@anonymized.local',
'phone' => null,
]);
$this->delete(); // soft-delete after anonymization
}
}3. Add data export endpoint (Article 20: Right to Data Portability):
// app/Http/Controllers/UserDataExportController.php
class UserDataExportController extends Controller
{
public function export(Request $request): JsonResponse
{
$user = $request->user();
$data = [
'personal' => $user->only(['name', 'email', 'phone']),
'orders' => $user->orders->toArray(),
'consents' => $user->consents->toArray(),
];
return response()->json($data)
->header('Content-Disposition', 'attachment; filename="my-data.json"');
}
}// routes/web.php
Route::get('/user/export', [UserDataExportController::class, 'export'])
->middleware('auth')
->name('user.data.export');4. Add PII protection to logging:
// app/Logging/SanitizePiiProcessor.php
use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;
class SanitizePiiProcessor implements ProcessorInterface
{
private array $keys = ['email', 'phone', 'ip', 'password', 'token'];
public function __invoke(LogRecord $record): LogRecord
{
$extra = $record->extra;
foreach ($this->keys as $key) {
if (isset($extra[$key])) {
$extra[$key] = '***';
}
}
return $record->with(extra: $extra);
}
}// config/logging.php — register the processor on your default channel
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily'],
'processors' => [SanitizePiiProcessor::class],
],
],References
- GDPR Official Text
- GDPR Article 17 - Right to Erasure
- GDPR Article 20 - Right to Data Portability
- Laravel Encryption Casts
- Laravel Model Pruning (SoftDeletes)
- OWASP Privacy Cheat Sheet
Related Analyzers
- Audit Logging - Validates audit trail logging
- Cookie - Checks cookie security configuration
- Telescope Security - Validates debug tool security