# 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:** ```python # 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:** ```php // 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:** ```python # 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:** ```php // 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:** ```php 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):** ```python 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 1. **SSRF Risk in Connection Testing** - No IP validation 2. **credentials_json Exposed in Serializer** - All fields returned 3. **Flexible ID Lookup Enables Enumeration** - `/post-status/{id}` accepts both IDs 4. **Excessive API Request Logging** - Keys in logs 5. **WordPress URL Not Validated** - No HTTPS enforcement 6. **API Key Partially Visible** - `/verify-key` shows 15-char prefix ### 4.4 LOW SEVERITY ISSUES 1. Version disclosure in multiple endpoints 2. No CORS headers defined on plugin endpoints 3. 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 1. **Duplicate Connection Testing** - Two endpoints with same logic 2. **Duplicate API Key Validation** - Same validation in 4+ files 3. **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_key - `backend/igny8_core/business/integration/models.py` - SiteIntegration, SyncEvent - `backend/igny8_core/business/publishing/models.py` - PublishingRecord - `backend/igny8_core/business/content/models.py` - Content, external fields - `backend/igny8_core/modules/integration/views.py` - API endpoints - `backend/igny8_core/modules/integration/webhooks.py` - Webhook handlers - `backend/igny8_core/business/publishing/services/publisher_service.py` - `backend/igny8_core/business/publishing/services/adapters/wordpress_adapter.py` - `backend/igny8_core/tasks/publishing_scheduler.py` - Celery tasks - `backend/igny8_core/tasks/wordpress_publishing.py` - Publishing task - `backend/igny8_core/celery.py` - Beat schedule ### Plugin Files - `plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php` - `plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-api.php` - `plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-webhooks.php` - `plugins/wordpress/source/igny8-wp-bridge/includes/functions.php` ### Documentation Files Reviewed - `docs/60-PLUGINS/WORDPRESS-INTEGRATION.md` - `docs/60-PLUGINS/INDEX.md` - `docs/60-PLUGINS/PLUGIN-UPDATE-WORKFLOW.md` - `docs/50-DEPLOYMENT/WORDPRESS-INTEGRATION-FLOW.md` - `docs/10-MODULES/PUBLISHER.md` - `docs/10-MODULES/INTEGRATIONS.md` - `docs/40-WORKFLOWS/CONTENT-PIPELINE.md` - `docs/40-WORKFLOWS/SCHEDULED-CONTENT-PUBLISHING.md` - `docs/00-SYSTEM/ARCHITECTURE.md` - `docs/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:** 1. Review findings with development team 2. Prioritize critical security fixes 3. Update documentation to match reality 4. Create migration plan for field removal 5. Implement encryption before production