24 KiB
IGNY8 ↔ WordPress Integration Complete Security Audit
Audit Date: 2026-01-13 Auditor: System Audit (Documentation Calibrated) Scope: Backend (Django) + WordPress Plugin + Documentation Review Status: COMPLETE - CALIBRATED WITH DOCUMENTATION
EXECUTIVE SUMMARY
This comprehensive audit examined the IGNY8 backend and WordPress plugin integration system, cross-referencing against all existing documentation. The audit identified 17 security vulnerabilities (3 critical, 5 high, 6 medium, 3 low), 6 unused/redundant database fields, 2 major data duplication issues, and several documentation vs. implementation gaps.
Key Findings:
- API keys stored in plain text (no encryption) despite documentation claims
- Public endpoints expose sensitive configuration data
- Timing attack vulnerability in Bearer token validation
- Documentation states "credentials encrypted at rest" but encryption NOT implemented
- Several fields documented but never used
- Missing scheduled publishing task from Celery Beat (documented but not configured)
PART 1: DOCUMENTATION VS IMPLEMENTATION GAPS
1.1 Critical Discrepancies Found
| Documentation Claim | Reality | Impact |
|---|---|---|
| "API keys encrypted at rest" (INTEGRATIONS.md) | Plain text CharField in database | CRITICAL - False security assumption |
| "Webhook signature verification" (INTEGRATIONS.md) | No signature verification implemented | HIGH - Anyone with key can spoof webhooks |
| "Rate limiting on webhook endpoint" (INTEGRATIONS.md) | Webhooks explicitly disable throttling (NoThrottle) |
HIGH - DoS possible |
| "auto_publish_enabled triggers WordPress push" | Automation Stage 7 sets status but does NOT trigger WP push | MEDIUM - Manual publish required |
| "Scheduled auto-publish task active" | Task exists but NOT in Celery Beat schedule | MEDIUM - Content sits unpublished |
| SiteIntegration.api_key field (INTEGRATIONS.md line 97) | API key stored in Site.wp_api_key, NOT SiteIntegration | Documentation outdated |
| "credentials_json stores WordPress credentials" | credentials_json is empty for WordPress; Site.wp_api_key is source of truth | Documentation outdated |
1.2 Documentation Accuracy Summary
| Document | Accuracy | Issues Found |
|---|---|---|
| WORDPRESS-INTEGRATION-FLOW.md | 90% | Accurate on auth, slight gaps on scheduling |
| WORDPRESS-INTEGRATION.md | 85% | Missing encryption truth, good on plugin distribution |
| PUBLISHER.md | 95% | Accurate on models and flows |
| INTEGRATIONS.md | 70% | Contains outdated field references, false encryption claim |
| SCHEDULED-CONTENT-PUBLISHING.md | 95% | Accurate on Celery tasks, correctly notes site_status vs status |
| CONTENT-PIPELINE.md | 90% | Accurate on stages, slight gap on Stage 8 auto-publish |
PART 2: SYSTEM ARCHITECTURE (VERIFIED)
2.1 Actual Data Flow (Verified Against Code)
IGNY8 Backend WordPress Plugin
┌──────────────────────┐ ┌──────────────────────┐
│ Site Model │ │ wp_options │
│ ├─ wp_api_key ◄──────┼────────────────┼─► igny8_api_key │
│ │ (SINGLE SOURCE) │ │ igny8_site_id │
│ ├─ domain │ │ igny8_integration_id│
│ └─ wp_url (LEGACY) │ │ │
│ │ │ Post Meta │
│ SiteIntegration │ │ ├─ _igny8_content_id │
│ ├─ config_json │ │ ├─ _igny8_task_id │
│ │ (site_url only) │ │ └─ _igny8_last_synced│
│ ├─ credentials_json │ │ │
│ │ (EMPTY for WP!) │ │ │
│ └─ sync_status │ │ │
│ │ │ │
│ Content Model │ │ │
│ ├─ external_id ◄─────┼────────────────┼─► post_id │
│ ├─ external_url │ │ │
│ ├─ status (editorial)│ │ │
│ └─ site_status │ │ │
│ (publishing) │ │ │
└──────────────────────┘ └──────────────────────┘
2.2 Authentication Flow (Verified)
IGNY8 → WordPress:
# publisher_service.py (line 136, 144)
destination_config = {
'api_key': site.wp_api_key, # FROM SITE MODEL
'site_url': site.domain or site.wp_url # Fallback to legacy
}
# wordpress_adapter.py
headers = {
'X-IGNY8-API-KEY': api_key, # Primary method
'Content-Type': 'application/json'
}
WordPress → IGNY8:
// Plugin: check_permission() method
$stored_api_key = get_option('igny8_api_key');
$header_api_key = $request->get_header('x-igny8-api-key');
// CORRECT: hash_equals for X-IGNY8-API-KEY
if (hash_equals($stored_api_key, $header_api_key)) return true;
// VULNERABLE: strpos for Bearer token
if (strpos($auth_header, 'Bearer ' . $stored_api_key) !== false) return true;
2.3 API Key Generation (Verified)
POST /api/v1/integration/integrations/generate-api-key/
Body: { "site_id": 123 }
Key Format: igny8_site_{site_id}_{timestamp_ms}_{random_10_chars}
Example: igny8_site_123_1736780400000_a7b9c3d2e1
Storage: Site.wp_api_key (CharField, plain text, max 255)
Recovery: NOT POSSIBLE - shown once on generation
Revocation: Sets Site.wp_api_key = None
PART 3: CONTENT STATUS SYSTEM (DOCUMENTED CORRECTLY)
3.1 Two-Status Architecture
The documentation correctly describes the dual-status system:
| Field | Purpose | Values |
|---|---|---|
Content.status |
Editorial workflow | draft → review → approved → (published - legacy) |
Content.site_status |
WordPress publishing | not_published → scheduled → publishing → published / failed |
3.2 Publishing Flow Paths
Path 1: Manual Publish (Working)
User clicks "Publish" → ContentViewSet.publish_to_wordpress
→ Celery task: publish_content_to_wordpress
→ WordPress API call
→ Update external_id, external_url, site_status='published'
Path 2: Scheduled Publish (Partially Working)
schedule_approved_content (hourly)
→ Find approved content with site_status='not_published'
→ Assign scheduled_publish_at
→ Set site_status='scheduled'
process_scheduled_publications (every 5 min)
→ Find content where scheduled_publish_at <= now
→ Queue publish_content_to_wordpress task
→ WordPress API call
Path 3: Automation Stage 7 (NOT TRIGGERING WP PUSH)
Automation Stage 7:
→ Content.status = 'published' (legacy status change)
→ NO site_status change
→ NO WordPress API call queued
→ Content sits with site_status='not_published'
GAP IDENTIFIED: Automation Stage 7 does NOT call publishing scheduler or queue WordPress task.
PART 4: SECURITY VULNERABILITIES (VERIFIED)
4.1 CRITICAL ISSUES
CRITICAL-1: API Keys Stored in Plain Text
Location: Backend - Site.wp_api_key field (auth/models.py line 491)
Documentation Claim: "Credentials encrypted at rest" (INTEGRATIONS.md line 345)
Reality: CharField stores plain text - NO encryption
Evidence:
# auth/models.py
wp_api_key = models.CharField(max_length=255, blank=True, null=True,
help_text="API key for WordPress integration via IGNY8 WP Bridge plugin")
Impact: Database compromise exposes ALL WordPress API keys Risk Score: 9/10
CRITICAL-2: Timing Attack in Bearer Token Validation
Location: Plugin - class-igny8-rest-api.php:140
Impact: API key can be guessed character-by-character
Vulnerable Code:
// Uses strpos (VULNERABLE)
if (strpos($auth_header, 'Bearer ' . $stored_api_key) !== false)
// Should use hash_equals (SAFE)
if (hash_equals($stored_api_key, substr($auth_header, 7)))
Risk Score: 8/10
CRITICAL-3: Diagnostic Logging Exposes Sensitive Data
Location: Plugin - class-igny8-rest-api.php:533-565
Impact: Full request bodies logged including all content
Evidence:
error_log('========== RAW REQUEST BODY ==========');
error_log($raw_body);
error_log('========== PARSED JSON DATA ==========');
error_log(print_r($content_data, true));
Risk Score: 8/10
4.2 HIGH SEVERITY ISSUES
HIGH-1: Public Endpoints Expose Configuration
Verified Endpoints:
| Endpoint | Permission | Data Exposed |
|---|---|---|
/wp-json/igny8/v1/status |
__return_true (PUBLIC) |
has_api_key, connected, versions |
/wp-json/igny8/v1/site-metadata/ |
__return_true (PUBLIC) |
post_types, taxonomies, counts |
Note: Documentation (WORDPRESS-INTEGRATION-FLOW.md line 86-91) does NOT flag these as security issues.
HIGH-2: Permission Architecture Inconsistency
Location: test_connection_collection endpoint
Permission: AllowAny (then checks auth manually inside view)
Documentation: Does not mention this inconsistency.
HIGH-3: No Webhook Signature Verification
Documentation Claim: "Optional signature verification" (INTEGRATIONS.md line 349) Reality: Only API key validation, NO HMAC signature verification
Code Evidence (webhooks.py):
api_key = request.headers.get('X-IGNY8-API-KEY')
if not stored_api_key or stored_api_key != api_key:
return error_response('Invalid API key', ...)
# NO signature validation
HIGH-4: Webhooks Disable Rate Limiting
Documentation Claim: "Rate limiting on webhook endpoint" (INTEGRATIONS.md line 351)
Reality: Webhooks explicitly use NoThrottle class
HIGH-5: Encryption Falls Back Silently
Location: Plugin - functions.php:31-48
Impact: Admin unaware if encryption fails
4.3 MEDIUM SEVERITY ISSUES
- SSRF Risk in Connection Testing - No IP validation
- credentials_json Exposed in Serializer - All fields returned
- Flexible ID Lookup Enables Enumeration -
/post-status/{id}accepts both IDs - Excessive API Request Logging - Keys in logs
- WordPress URL Not Validated - No HTTPS enforcement
- API Key Partially Visible -
/verify-keyshows 15-char prefix
4.4 LOW SEVERITY ISSUES
- Version disclosure in multiple endpoints
- No CORS headers defined on plugin endpoints
- Webhook logs stored long-term (500 entries)
PART 5: REST API ENDPOINTS INVENTORY (VERIFIED)
5.1 Backend Endpoints (Verified Against urls.py)
| Endpoint | Method | Permission | Documentation |
|---|---|---|---|
/api/v1/integration/integrations/ |
CRUD | IsAuthenticatedAndActive + IsEditorOrAbove | Correct |
/api/v1/integration/integrations/test-connection/ (collection) |
POST | AllowAny | NOT documented as AllowAny |
/api/v1/integration/integrations/{id}/test-connection/ |
POST | Authenticated | Correct |
/api/v1/integration/integrations/generate-api-key/ |
POST | Authenticated | Correct |
/api/v1/integration/integrations/revoke-api-key/ |
POST | Authenticated | Correct |
/api/v1/integration/webhooks/wordpress/status/ |
POST | AllowAny (header auth) | Correct |
/api/v1/integration/webhooks/wordpress/metadata/ |
POST | AllowAny (header auth) | Correct |
/api/v1/publisher/publish/ |
POST | Authenticated | Correct |
/api/v1/writer/content/{id}/publish_to_wordpress/ |
POST | Authenticated | Correct |
/api/v1/writer/content/{id}/schedule/ |
POST | Authenticated | Correct (v1.3.2) |
/api/v1/writer/content/{id}/unschedule/ |
POST | Authenticated | Correct (v1.3.2) |
5.2 Plugin Endpoints (Verified Against class-igny8-rest-api.php)
| Endpoint | Method | Permission | Documentation Match |
|---|---|---|---|
/wp-json/igny8/v1/status |
GET | PUBLIC | Not flagged as security issue |
/wp-json/igny8/v1/site-metadata/ |
GET | PUBLIC (internal check) | Not flagged |
/wp-json/igny8/v1/verify-key |
GET | check_permission | Correct |
/wp-json/igny8/v1/publish |
POST | check_permission | Correct |
/wp-json/igny8/v1/post-by-content-id/{id} |
GET | check_permission | Correct |
/wp-json/igny8/v1/post-by-task-id/{id} |
GET | check_permission | Correct |
/wp-json/igny8/v1/post-status/{id} |
GET | check_permission | Correct |
/wp-json/igny8/v1/event |
POST | verify_webhook_secret | Correct |
PART 6: DATA STORAGE ANALYSIS (VERIFIED)
6.1 Site Model Fields (auth/models.py)
| Field | Documentation Status | Actual Usage | Action |
|---|---|---|---|
wp_api_key |
Documented as primary | ACTIVE - single source of truth | KEEP + ENCRYPT |
domain |
Documented | ACTIVE - WordPress URL | KEEP |
wp_url |
Documented as "deprecated" | LEGACY - fallback in publisher_service | EVALUATE for removal |
wp_username |
Documented as "deprecated" | ZERO USAGE in codebase | REMOVE |
wp_app_password |
Documented as "deprecated" | ZERO USAGE in codebase | REMOVE |
6.2 SiteIntegration Model Fields
| Field | Documentation Status | Actual Usage | Action |
|---|---|---|---|
site |
Documented | ACTIVE | KEEP |
platform |
Documented | ACTIVE ('wordpress') | KEEP |
config_json |
Documented as storing URL | ACTIVE (site_url only) | KEEP |
credentials_json |
Documented as storing creds | EMPTY for WordPress | Documentation outdated |
sync_status |
Documented | ACTIVE | KEEP |
Documentation Gap: INTEGRATIONS.md line 97-98 shows api_key and username as SiteIntegration fields, but WordPress actually uses Site.wp_api_key.
6.3 Content Model Fields
| Field | Documentation Status | Actual Usage | Action |
|---|---|---|---|
status |
Correctly documented as editorial | ACTIVE | KEEP |
site_status |
Correctly documented as publishing | ACTIVE | KEEP |
external_id |
Documented | ACTIVE - WordPress post ID | KEEP |
external_url |
Documented | ACTIVE - WordPress URL | KEEP |
external_type |
Not documented | NEVER USED | REMOVE |
external_metadata |
Not documented | Only set to {} | REMOVE |
sync_status (on Content) |
Not documented | NEVER USED | REMOVE |
6.4 ContentTaxonomy Model Fields
| Field | Documentation Status | Actual Usage | Action |
|---|---|---|---|
external_id |
Documented | ACTIVE | KEEP |
external_taxonomy |
Documented | ACTIVE | KEEP |
sync_status |
Not documented | NEVER USED | REMOVE |
PART 7: CELERY TASKS STATUS (VERIFIED)
7.1 Scheduled Tasks (celery.py Beat Schedule)
| Task | Schedule | Documentation | Status |
|---|---|---|---|
schedule_approved_content |
Every hour | Documented (SCHEDULED-CONTENT-PUBLISHING.md) | ACTIVE |
process_scheduled_publications |
Every 5 min | Documented | ACTIVE |
publish_content_to_wordpress |
On-demand | Documented | ACTIVE |
7.2 Missing Tasks (Documented but NOT Scheduled)
process_pending_wordpress_publications - WORDPRESS-INTEGRATION-FLOW.md mentions this task exists but notes:
"CURRENT STATUS: This task is NOT in Celery Beat schedule!"
This matches the documentation correctly.
PART 8: PLUGIN DISTRIBUTION SYSTEM (VERIFIED)
8.1 Plugin Models (Correctly Documented)
| Model | Documentation | Implementation | Match |
|---|---|---|---|
Plugin |
WORDPRESS-INTEGRATION.md | plugins/models.py | ✅ |
PluginVersion |
Documented with all fields | Matches implementation | ✅ |
PluginInstallation |
Documented | Matches implementation | ✅ |
PluginDownload |
Documented | Matches implementation | ✅ |
8.2 Plugin API Endpoints (Correctly Documented)
| Endpoint | Documentation | Implementation |
|---|---|---|
/api/plugins/{slug}/download/ |
INDEX.md line 24 | ✅ Working |
/api/plugins/{slug}/check-update/ |
INDEX.md line 25 | ✅ Working |
/api/plugins/{slug}/info/ |
INDEX.md line 26 | ✅ Working |
/api/plugins/{slug}/register/ |
INDEX.md line 27 | ✅ Working |
/api/plugins/{slug}/health-check/ |
INDEX.md line 28 | ✅ Working |
PART 9: UNUSED/DEAD CODE INVENTORY
9.1 Fields to Remove (Verified Zero Usage)
| Model | Field | Evidence |
|---|---|---|
| Site | wp_username |
grep -r "wp_username" --include="*.py" = 0 results |
| Site | wp_app_password |
grep -r "wp_app_password" --include="*.py" = 0 results |
| Content | external_type |
Never read in codebase |
| Content | external_metadata |
Only set to {} in publisher_service.py |
| Content | sync_status |
SiteIntegration.sync_status used instead |
| ContentTaxonomy | sync_status |
Never read or written |
9.2 Redundant Code Paths
- Duplicate Connection Testing - Two endpoints with same logic
- Duplicate API Key Validation - Same validation in 4+ files
- Dead Admin Bulk Actions -
bulk_trigger_sync(),bulk_test_connection()have TODO comments
9.3 Plugin Unused Options
| Option | Status |
|---|---|
igny8_access_token |
Redundant with igny8_api_key |
igny8_access_token_issued |
Referenced but not used in auth |
PART 10: DATA DUPLICATION ISSUES
10.1 API Key Duplication
| Location | Field | Role |
|---|---|---|
| Django Site model | wp_api_key |
PRIMARY (single source of truth) |
| Django SiteIntegration | credentials_json |
EMPTY for WordPress |
| WordPress | igny8_api_key |
COPY (must match primary) |
| WordPress | igny8_access_token |
REDUNDANT (should remove) |
Risk: Documentation mentions credentials_json but WordPress doesn't use it.
10.2 URL Duplication
| Location | Field | Role |
|---|---|---|
| Site.domain | Primary | ACTIVE |
| Site.wp_url | Legacy | Fallback only |
| SiteIntegration.config_json['site_url'] | Configuration | ACTIVE |
PART 11: RECOMMENDATIONS
Immediate Actions (Critical)
| Priority | Issue | Action | Documentation Update |
|---|---|---|---|
| 1 | Plain text API keys | Implement field encryption | Update INTEGRATIONS.md |
| 2 | Timing attack (strpos) | Use hash_equals everywhere | Update plugin docs |
| 3 | Diagnostic logging | Remove or conditional | Update plugin docs |
| 4 | Public endpoints | Secure /status and /site-metadata | Update WORDPRESS-INTEGRATION-FLOW.md |
Short-term Actions (High)
| Priority | Issue | Action | Documentation Update |
|---|---|---|---|
| 5 | Permission inconsistency | Fix test_connection_collection | Update ENDPOINTS.md |
| 6 | No webhook signatures | Implement HMAC verification | Update INTEGRATIONS.md |
| 7 | No rate limiting | Enable throttling on webhooks | Update INTEGRATIONS.md |
| 8 | API key in response | Remove from verify-key | N/A |
Documentation Updates Required
| Document | Updates Needed |
|---|---|
| INTEGRATIONS.md | Remove "encrypted at rest" claim, fix field references |
| WORDPRESS-INTEGRATION-FLOW.md | Flag public endpoints as security concern |
| ENDPOINTS.md | Note AllowAny on test-connection collection |
Cleanup Actions
| Action | Fields to Remove | Migration Required |
|---|---|---|
| Remove from Site | wp_username, wp_app_password | Yes |
| Remove from Content | external_type, external_metadata, sync_status | Yes |
| Remove from ContentTaxonomy | sync_status | Yes |
| Remove from Plugin | igny8_access_token option | Plugin update |
PART 12: PUBLISHING WORKFLOW SUMMARY
What's Working
| Flow | Status | Notes |
|---|---|---|
| Manual publish button | ✅ Working | ContentViewSet.publish_to_wordpress |
| Scheduled publishing | ✅ Working | Celery Beat tasks active |
| Plugin distribution | ✅ Working | Auto-update mechanism functional |
| Webhook status sync | ✅ Working | WordPress → IGNY8 updates |
What's NOT Working
| Flow | Status | Issue |
|---|---|---|
| Automation Stage 7 → WP | ❌ Broken | Sets status but no WP push |
| Content update sync | ❌ Missing | No republish capability |
| WordPress → IGNY8 import | ❌ Missing | No pull sync feature |
Documented But Not Implemented
| Feature | Documentation Reference | Status |
|---|---|---|
| Webhook signature verification | INTEGRATIONS.md line 349 | NOT implemented |
| Webhook rate limiting | INTEGRATIONS.md line 351 | NOT implemented |
| Credential encryption | INTEGRATIONS.md line 345 | NOT implemented |
APPENDIX A: FILES AUDITED
Backend Files
backend/igny8_core/auth/models.py- Site model, wp_api_keybackend/igny8_core/business/integration/models.py- SiteIntegration, SyncEventbackend/igny8_core/business/publishing/models.py- PublishingRecordbackend/igny8_core/business/content/models.py- Content, external fieldsbackend/igny8_core/modules/integration/views.py- API endpointsbackend/igny8_core/modules/integration/webhooks.py- Webhook handlersbackend/igny8_core/business/publishing/services/publisher_service.pybackend/igny8_core/business/publishing/services/adapters/wordpress_adapter.pybackend/igny8_core/tasks/publishing_scheduler.py- Celery tasksbackend/igny8_core/tasks/wordpress_publishing.py- Publishing taskbackend/igny8_core/celery.py- Beat schedule
Plugin Files
plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.phpplugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-api.phpplugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-webhooks.phpplugins/wordpress/source/igny8-wp-bridge/includes/functions.php
Documentation Files Reviewed
docs/60-PLUGINS/WORDPRESS-INTEGRATION.mddocs/60-PLUGINS/INDEX.mddocs/60-PLUGINS/PLUGIN-UPDATE-WORKFLOW.mddocs/50-DEPLOYMENT/WORDPRESS-INTEGRATION-FLOW.mddocs/10-MODULES/PUBLISHER.mddocs/10-MODULES/INTEGRATIONS.mddocs/40-WORKFLOWS/CONTENT-PIPELINE.mddocs/40-WORKFLOWS/SCHEDULED-CONTENT-PUBLISHING.mddocs/00-SYSTEM/ARCHITECTURE.mddocs/20-API/ENDPOINTS.md
APPENDIX B: VULNERABILITY SEVERITY MATRIX
| ID | Title | CVSS | Documentation Claim | Reality |
|---|---|---|---|---|
| CRITICAL-1 | Plain text API keys | 9.0 | "Encrypted at rest" | Plain text CharField |
| CRITICAL-2 | Timing attack | 8.0 | Not mentioned | strpos vulnerability |
| CRITICAL-3 | Diagnostic logging | 8.0 | Not mentioned | Full request bodies logged |
| HIGH-1 | Public endpoints | 7.0 | Not flagged | Information disclosure |
| HIGH-2 | Permission inconsistency | 6.5 | Not documented | AllowAny misuse |
| HIGH-3 | No webhook signatures | 6.0 | "Optional" | Not implemented at all |
| HIGH-4 | No rate limiting | 5.5 | "Rate limiting enabled" | NoThrottle class used |
| HIGH-5 | Silent encryption fail | 6.0 | Not mentioned | Falls back to plain text |
End of Calibrated Audit Report
Next Steps:
- Review findings with development team
- Prioritize critical security fixes
- Update documentation to match reality
- Create migration plan for field removal
- Implement encryption before production