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-SignatureX-Hub-Signature-256(GitHub format)X-WP-Webhook-Signature(WordPress format)X-Content-SignatureSignature
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:
- Ensure HTMLPurifier is installed in production
- Add package check in boot to fail loudly if missing
- Consider using
voku/anti-xssas a lighter alternative
HIGH: Webhook Signature Optional
Location: Models/ContentWebhookEndpoint.php:205-210
Issue: When no secret is configured, signature verification is skipped:
if (empty($this->secret)) {
return true; // Accepts all requests
}Risk: Unauthenticated webhook injection if endpoint has no secret.
Mitigation:
- Require secrets for all production endpoints
- Add explicit
allow_unsignedflag if intentional - Log warning when unsigned webhooks are accepted
- Rate limit unsigned endpoints more aggressively
MEDIUM: Workspace Access in MCP Handlers
Location: Mcp/Handlers/*.php
Issue: Workspace resolution allows lookup by ID:
return Workspace::where('slug', $slug)
->orWhere('id', $slug)
->first();Risk: If an attacker knows a workspace ID, they could potentially access content without being a workspace member.
Mitigation:
- Always verify workspace membership after resolution
- Use entitlement checks (already present but verify coverage)
- 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:
- Add rate limiting (30/min per user)
- Use constant-time responses regardless of content existence
- 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:
$contentItem->content_type = ContentType::NATIVE;Risk: External systems could potentially inject invalid content types.
Mitigation:
- Validate against
ContentTypeenum - Default to a safe type if validation fails
- 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 toencrypted)ContentWebhookEndpoint.previous_secret(cast toencrypted)
Hidden from serialisation:
- Webhook secrets (via
$hiddenproperty)
PII Considerations
Content may contain PII in:
- Article body content
- Author information
- Webhook payloads
Recommendations:
- Implement content retention policies
- Add GDPR data export/deletion support
- 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
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 uploadsAutomated 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.