Security¶
This document covers security considerations, known risks, and recommended mitigations for the core-content package.
Authentication and Authorisation¶
API Authentication¶
The content API supports two authentication methods:
- Session Authentication (
authmiddleware) - For browser-based access
-
CSRF protection via Laravel's standard middleware
-
API Key Authentication (
api.authmiddleware) - For programmatic access
- Keys prefixed with
hk_ - Scope enforcement via
api.scope.enforcemiddleware
Webhook Authentication¶
Webhooks use HMAC signature verification instead of session/API key auth:
// Signature verification in ContentWebhookEndpoint
public function verifySignature(string $payload, ?string $signature): bool
{
$expectedSignature = hash_hmac('sha256', $payload, $this->secret);
return hash_equals($expectedSignature, $signature);
}
Supported signature headers:
- X-Signature
- X-Hub-Signature-256 (GitHub format)
- X-WP-Webhook-Signature (WordPress format)
- X-Content-Signature
- Signature
MCP Tool Authentication¶
MCP tools authenticate via the MCP session context. Workspace access is verified through:
- Workspace resolution (by slug or ID)
- Entitlement checks (content.mcp_access, content.items)
Known Security Considerations¶
HIGH: HTML Sanitisation Fallback¶
Location: Models/ContentItem.php:333-351
Issue: The getSanitisedContent() method falls back to strip_tags() if HTMLPurifier is unavailable. This is insufficient for XSS protection.
// Current fallback (insufficient)
$allowedTags = '<p><br><strong>...<a>...';
return strip_tags($content, $allowedTags);
Risk: XSS attacks via crafted HTML in content body.
Mitigation:
1. Ensure HTMLPurifier is installed in production
2. Add package check in boot to fail loudly if missing
3. Consider using voku/anti-xss as a lighter alternative
HIGH: Webhook Signature Optional¶
Location: Models/ContentWebhookEndpoint.php:205-210
Issue: When no secret is configured, signature verification is skipped:
Risk: Unauthenticated webhook injection if endpoint has no secret.
Mitigation:
1. Require secrets for all production endpoints
2. Add explicit allow_unsigned flag if intentional
3. Log warning when unsigned webhooks are accepted
4. Rate limit unsigned endpoints more aggressively
MEDIUM: Workspace Access in MCP Handlers¶
Location: Mcp/Handlers/*.php
Issue: Workspace resolution allows lookup by ID:
Risk: If an attacker knows a workspace ID, they could potentially access content without being a workspace member.
Mitigation: 1. Always verify workspace membership after resolution 2. Use entitlement checks (already present but verify coverage) 3. Consider removing ID-based lookup for MCP
MEDIUM: Preview Token Enumeration¶
Location: Controllers/ContentPreviewController.php
Issue: No rate limiting on preview token generation endpoint. An attacker could probe for valid content IDs.
Mitigation: 1. Add rate limiting (30/min per user) 2. Use constant-time responses regardless of content existence 3. Consider using UUIDs instead of sequential IDs for preview URLs
LOW: Webhook Payload Content Types¶
Location: Jobs/ProcessContentWebhook.php:288-289
Issue: Content type from external webhook is assigned directly:
Risk: External systems could potentially inject invalid content types.
Mitigation:
1. Validate against ContentType enum
2. Default to a safe type if validation fails
3. Log invalid types for monitoring
Input Validation¶
API Request Validation¶
All API controllers use Laravel's validation:
$validated = $request->validate([
'q' => 'required|string|min:2|max:500',
'type' => 'nullable|string|in:post,page',
'status' => 'nullable',
// ...
]);
Validated inputs: - Search queries (min/max length, string type) - Content types (enum validation) - Pagination (min/max values) - Date ranges (date format, logical order)
MCP Input Validation¶
MCP handlers validate via JSON schema:
'inputSchema' => [
'type' => 'object',
'properties' => [
'workspace' => ['type' => 'string'],
'title' => ['type' => 'string'],
'type' => ['type' => 'string', 'enum' => ['post', 'page']],
],
'required' => ['workspace', 'title'],
]
Webhook Payload Validation¶
Webhook payloads undergo: - JSON decode validation - Event type normalisation - Content ID extraction with fallbacks
Note: Payload content is stored in JSON column without full validation. Processing logic handles missing/invalid fields gracefully.
Rate Limiting¶
Configured Limiters¶
| Endpoint | Auth | Unauthenticated | Key |
|---|---|---|---|
| AI Generation | 10/min | 2/min | content-generate |
| Brief Creation | 30/min | 5/min | content-briefs |
| Webhooks | 60/min | 30/min | content-webhooks |
| Search | 60/min | 20/min | content-search |
Rate Limit Bypass Risks¶
- IP Spoofing: Ensure
X-Forwarded-Forhandling is configured correctly - Workspace Switching: Workspace-based limits should use user ID as fallback
- API Key Sharing: Each key should have independent limits
Data Protection¶
Sensitive Data Handling¶
Encrypted at rest:
- ContentWebhookEndpoint.secret (cast to encrypted)
- ContentWebhookEndpoint.previous_secret (cast to encrypted)
Hidden from serialisation:
- Webhook secrets (via $hidden property)
PII Considerations¶
Content may contain PII in: - Article body content - Author information - Webhook payloads
Recommendations: 1. Implement content retention policies 2. Add GDPR data export/deletion support 3. Log access to PII-containing content
Webhook Security¶
Circuit Breaker¶
Endpoints automatically disable after 10 consecutive failures:
const MAX_FAILURES = 10;
public function incrementFailureCount(): void
{
$this->increment('failure_count');
if ($this->failure_count >= self::MAX_FAILURES) {
$this->update(['is_enabled' => false]);
}
}
Secret Rotation¶
Grace period support for secret rotation:
public function isInGracePeriod(): bool
{
// Accepts both current and previous secret during grace
}
Default grace period: 24 hours
Allowed Event Types¶
Endpoints can restrict which event types they accept:
const ALLOWED_TYPES = [
'wordpress.post_created',
'wordpress.post_updated',
// ...
'generic.payload',
];
Wildcard support: wordpress.* matches all WordPress events.
Content Security¶
XSS Prevention¶
- Input: Content stored as-is to preserve formatting
- Output:
getSanitisedContent()for public rendering - Admin: Trusted content displayed with proper escaping
Blade template guidelines:
- Use {{ $title }} for plain text (auto-escaped)
- Use {!! $content !!} only for sanitised HTML
- Comments document which fields need which treatment
SQL Injection¶
All database queries use: - Eloquent ORM (parameterised queries) - Query builder with bindings - No raw SQL with user input
CSRF Protection¶
Web routes include CSRF middleware automatically. API routes exempt (use API key auth).
Audit Logging¶
Logged Events¶
- Webhook receipt and processing
- AI generation requests and results
- Content creation/update/deletion via MCP
- CDN cache purges
- Authentication failures
Log Levels¶
| Event | Level |
|---|---|
| Webhook signature failure | WARNING |
| Circuit breaker triggered | WARNING |
| Processing failure | ERROR |
| Successful operations | INFO |
| Skipped operations | DEBUG |
Recommendations¶
Immediate (P1)¶
- [ ] Require HTMLPurifier or equivalent in production
- [ ] Make webhook signature verification mandatory
- [ ] Add rate limiting to preview generation
- [ ] Validate content_type from webhook payloads
Short-term (P2)¶
- [ ] Add comprehensive audit logging
- [ ] Implement content access logging
- [ ] Add IP allowlisting option for webhooks
- [ ] Create security-focused test suite
Long-term (P3+)¶
- [ ] Implement content encryption at rest option
- [ ] Add GDPR compliance features
- [ ] Create security monitoring dashboard
- [ ] Add anomaly detection for webhook patterns
Security Testing¶
Manual Testing Checklist¶
[ ] Verify webhook signature rejection with invalid signature
[ ] Test rate limiting enforcement
[ ] Confirm XSS payloads are sanitised
[ ] Verify workspace isolation in API responses
[ ] Test preview token expiration
[ ] Verify CSRF protection on web routes
[ ] Test SQL injection attempts in search
[ ] Verify file type validation on media uploads
Automated Testing¶
# Run security-focused tests
./vendor/bin/pest --filter=Security
# Check for common vulnerabilities
./vendor/bin/pint --test # Code style (includes some security patterns)
Incident Response¶
Webhook Compromise¶
- Disable affected endpoint
- Rotate all secrets
- Review webhook logs for suspicious patterns
- Regenerate secrets for all endpoints
Content Injection¶
- Identify affected content items
- Restore from revision history
- Review webhook source
- Add additional validation
API Key Leak¶
- Revoke compromised key
- Review access logs
- Generate new key with reduced scope
- Monitor for unauthorised access
Contact¶
Security issues should be reported to the security team. Do not create public issues for security vulnerabilities.