php-framework/docs/packages/content/security.md

390 lines
9.7 KiB
Markdown
Raw Permalink Normal View History

---
title: Security
description: Security considerations and audit notes for core-content
updated: 2026-01-29
---
# 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:
1. **Session Authentication** (`auth` middleware)
- For browser-based access
- CSRF protection via Laravel's standard middleware
2. **API Key Authentication** (`api.auth` middleware)
- For programmatic access
- Keys prefixed with `hk_`
- Scope enforcement via `api.scope.enforce` middleware
### Webhook Authentication
Webhooks use HMAC signature verification instead of session/API key auth:
```php
// 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.
```php
// 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:
```php
if (empty($this->secret)) {
return true; // Accepts all requests
}
```
**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:
```php
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:**
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:
```php
$contentItem->content_type = ContentType::NATIVE;
```
**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:
```php
$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:
```php
'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
1. **IP Spoofing:** Ensure `X-Forwarded-For` handling is configured correctly
2. **Workspace Switching:** Workspace-based limits should use user ID as fallback
3. **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:
```php
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:
```php
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:
```php
const ALLOWED_TYPES = [
'wordpress.post_created',
'wordpress.post_updated',
// ...
'generic.payload',
];
```
Wildcard support: `wordpress.*` matches all WordPress events.
## Content Security
### XSS Prevention
1. **Input:** Content stored as-is to preserve formatting
2. **Output:** `getSanitisedContent()` for public rendering
3. **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)
1. [ ] Require HTMLPurifier or equivalent in production
2. [ ] Make webhook signature verification mandatory
3. [ ] Add rate limiting to preview generation
4. [ ] Validate content_type from webhook payloads
### Short-term (P2)
1. [ ] Add comprehensive audit logging
2. [ ] Implement content access logging
3. [ ] Add IP allowlisting option for webhooks
4. [ ] Create security-focused test suite
### Long-term (P3+)
1. [ ] Implement content encryption at rest option
2. [ ] Add GDPR compliance features
3. [ ] Create security monitoring dashboard
4. [ ] 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
```bash
# 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
1. Disable affected endpoint
2. Rotate all secrets
3. Review webhook logs for suspicious patterns
4. Regenerate secrets for all endpoints
### Content Injection
1. Identify affected content items
2. Restore from revision history
3. Review webhook source
4. Add additional validation
### API Key Leak
1. Revoke compromised key
2. Review access logs
3. Generate new key with reduced scope
4. Monitor for unauthorised access
## Contact
Security issues should be reported to the security team. Do not create public issues for security vulnerabilities.