9.3 KiB
IGNY8 ↔ WordPress Integration Security Plan
Date: 2026-01-13
Status: Audit Complete - Plan Ready
PART 1: ACTUAL ARCHITECTURE (VERIFIED)
1.1 Publishing Flow (WORKING - No Changes Needed)
Frontend (Review/Approved pages)
│
▼
POST /v1/publisher/publish/
│
▼
PublisherService._publish_to_destination()
│
├── Gets API key: content.site.wp_api_key (SINGLE SOURCE OF TRUTH)
├── Gets URL: content.site.domain or content.site.url
│
▼
WordPressAdapter._publish_via_api_key()
│
▼
POST {site_url}/wp-json/igny8/v1/publish
Headers: X-IGNY8-API-KEY: {api_key}
│
▼
Plugin: check_permission() validates header against stored igny8_api_key
│
▼
Plugin creates/updates WordPress post
Key Finding: Publishing does NOT use SiteIntegration model. It uses:
Site.wp_api_key- for authenticationSite.domain- for WordPress URL
1.2 Data Storage Locations
| Location | Field | Purpose | Used By |
|---|---|---|---|
| Django Site model | wp_api_key |
API key (source of truth) | Publishing, test-connection |
| Django Site model | domain |
WordPress URL | Publishing |
| WordPress wp_options | igny8_api_key |
Stored copy of API key | Plugin auth check |
| WordPress wp_options | igny8_site_id |
Site ID from key | Plugin display |
| WordPress wp_options | igny8_integration_id |
Integration ID | Plugin (currently unused) |
| WordPress wp_options | igny8_last_structure_sync |
Last sync timestamp | Connection status UI |
1.3 SiteIntegration Model Status
Table exists: igny8_site_integrations
Records: 0 (empty)
Used by:
publishing_scheduler.py- scheduled publishing (requires record)wordpress_publishing.py- Celery task path (requires record)sync_metadata_service.py- metadata sync (requires record)ContentViewSet.publishaction (requires record)
NOT used by:
PublisherService(main UI publishing path) - uses Site directlyWordPressAdapter- uses Site.wp_api_key directly
PART 2: SECURITY ISSUES FOUND
2.1 Plugin Public Endpoints (SECURITY RISK)
| Endpoint | Current Permission | Risk | Data Exposed |
|---|---|---|---|
/igny8/v1/status |
__return_true (PUBLIC) |
Medium | Plugin installed, has_api_key, plugin version |
/igny8/v1/site-metadata/ |
__return_true (PUBLIC*) |
Low | Post types, taxonomies (auth checked inside) |
*Note: /site-metadata/ checks permission inside callback but still registers as public
2.2 Backend test-connection Flow
The test_connection_collection endpoint:
- Uses
AllowAnypermission class - BUT validates authentication inside the handler
- Calls public
/statusendpoint to check if plugin installed - Then calls authenticated
/verify-keyendpoint
Issue: Relies on public /status endpoint to detect plugin presence.
PART 3: RECOMMENDED CHANGES
3.1 Plugin Changes (REQUIRED)
3.1.1 Make /status Endpoint Authenticated
File: plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php
Current (lines 83-87):
register_rest_route('igny8/v1', '/status', array(
'methods' => 'GET',
'callback' => array($this, 'get_status'),
'permission_callback' => '__return_true', // Public endpoint for health checks
));
Change to:
register_rest_route('igny8/v1', '/status', array(
'methods' => 'GET',
'callback' => array($this, 'get_status'),
'permission_callback' => array($this, 'check_api_key_auth'),
));
3.1.2 Add Lightweight Auth Check Method
Add new method to allow API key OR no key if not configured yet:
/**
* Check API key authentication for status-type endpoints
* Returns true if: no API key stored yet, OR valid API key provided
*/
public function check_api_key_auth($request) {
$stored_api_key = function_exists('igny8_get_secure_option')
? igny8_get_secure_option('igny8_api_key')
: get_option('igny8_api_key');
// If no API key configured yet, allow access (plugin not connected)
if (empty($stored_api_key)) {
return true;
}
// If API key is configured, require valid key in header
$header_api_key = $request->get_header('x-igny8-api-key');
if ($header_api_key && hash_equals($stored_api_key, $header_api_key)) {
return true;
}
return new WP_Error('rest_forbidden', 'Invalid API key', array('status' => 401));
}
3.1.3 Fix /site-metadata/ Route Registration
Current (lines 74-79):
register_rest_route('igny8/v1', '/site-metadata/', array(
'methods' => 'GET',
'callback' => array($this, 'get_site_metadata'),
'permission_callback' => '__return_true',
));
Change to:
register_rest_route('igny8/v1', '/site-metadata/', array(
'methods' => 'GET',
'callback' => array($this, 'get_site_metadata'),
'permission_callback' => array($this, 'check_permission'),
));
Then remove the internal permission check from get_site_metadata() method.
3.2 Backend Changes (REQUIRED)
3.2.1 Update test-connection to Not Rely on Public Endpoint
File: backend/igny8_core/modules/integration/views.py
Current approach:
- Call
/status(public) to check plugin installed - Call
/verify-key(authenticated) to verify key
New approach:
- Call
/verify-keydirectly - If 200 → plugin installed AND key valid
- If 404 → plugin not installed (route doesn't exist)
- If 401/403 → plugin installed but key mismatch
Change Check 2 section (approximately lines 218-237):
# Check 2 & 3 Combined: Plugin installed + API key verification
# Use /verify-key endpoint - if it succeeds, both are confirmed
try:
verify_response = http_requests.get(
f"{site_url.rstrip('/')}/wp-json/igny8/v1/verify-key",
headers={
'X-IGNY8-API-KEY': stored_api_key,
'Content-Type': 'application/json'
},
timeout=10
)
if verify_response.status_code == 200:
health_checks['plugin_installed'] = True
health_checks['plugin_has_api_key'] = True
health_checks['api_key_verified'] = True
elif verify_response.status_code == 404:
# Route not found = plugin not installed
issues.append("IGNY8 plugin not installed on WordPress site")
elif verify_response.status_code in [401, 403]:
# Auth failed = plugin installed but key doesn't match
health_checks['plugin_installed'] = True
issues.append("API key mismatch - copy the API key from IGNY8 to WordPress plugin settings")
else:
issues.append(f"Unexpected response from plugin: HTTP {verify_response.status_code}")
except http_requests.exceptions.ConnectionError:
issues.append("Cannot connect to WordPress site")
except Exception as e:
issues.append(f"Connection error: {str(e)}")
3.3 SiteIntegration Decision (OPTIONAL)
The SiteIntegration model is used by:
- Scheduled publishing task
- ContentViewSet.publish action
- Metadata sync service
Options:
Option A: Remove SiteIntegration entirely
- Modify scheduled publishing to use Site directly
- Modify sync service to use Site directly
- Remove model and migrations
- Effort: High, risk of breaking things
Option B: Keep but don't require it
- Current main publishing path works without it
- Scheduled publishing would need records created
- Effort: Low, minimal changes
Recommendation: Option B - Keep as-is. The main UI publishing flow works. If scheduled publishing is needed, run:
docker exec igny8_backend python manage.py sync_wordpress_api_keys
PART 4: IMPLEMENTATION ORDER
Phase 1: Plugin Security (30 min)
- Add
check_api_key_auth()method to plugin - Change
/statusendpoint to usecheck_api_key_auth - Change
/site-metadata/to usecheck_permission - Remove internal permission check from
get_site_metadata() - Build and deploy plugin
Phase 2: Backend Update (15 min)
- Update
test_connection_collectionto use only/verify-key - Remove
/statusendpoint call - Test connection flow
Phase 3: Testing (15 min)
- Test plugin uninstalled scenario (404 from /verify-key)
- Test plugin installed, no key (401 from /verify-key)
- Test plugin installed, wrong key (401 from /verify-key)
- Test plugin installed, correct key (200 from /verify-key)
- Test publishing still works
PART 5: FILES TO MODIFY
Plugin Files
| File | Changes |
|---|---|
includes/class-igny8-rest-api.php |
Add check_api_key_auth(), update route permissions |
Backend Files
| File | Changes |
|---|---|
modules/integration/views.py |
Update test_connection_collection logic |
PART 6: WHAT STAYS THE SAME
✅ Publishing flow - No changes needed
✅ Site.wp_api_key - Single source of truth
✅ Site.domain - WordPress URL source
✅ WordPressAdapter - Works correctly
✅ Plugin /publish endpoint - Already authenticated
✅ Plugin /verify-key endpoint - Already authenticated
✅ SiteIntegration model - Keep for scheduled publishing (optional use)
Document History
| Date | Author | Changes |
|---|---|---|
| 2026-01-13 | AI | Created after proper code audit |