260 lines
8.4 KiB
Markdown
260 lines
8.4 KiB
Markdown
# Authentication Security Fixes - January 13, 2026
|
|
|
|
## Critical Security Issues Fixed
|
|
|
|
### Overview
|
|
Fixed multiple critical security vulnerabilities where API connections could appear successful without proper authentication, and status endpoints were publicly accessible without API key validation.
|
|
|
|
## Issues Fixed
|
|
|
|
### 1. Backend `/test-connection/` Endpoint Security
|
|
**Issue:** Backend endpoint allowed unauthenticated requests via `AllowAny` permission class
|
|
|
|
**Location:** `/data/app/igny8/backend/igny8_core/modules/integration/views.py`
|
|
|
|
**Before:**
|
|
```python
|
|
@action(detail=False, methods=['post'], url_path='test-connection',
|
|
permission_classes=[AllowAny], throttle_classes=[NoThrottle])
|
|
def test_connection_collection(self, request):
|
|
```
|
|
|
|
**After:**
|
|
```python
|
|
@action(detail=False, methods=['post'], url_path='test-connection')
|
|
def test_connection_collection(self, request):
|
|
```
|
|
|
|
**Impact:** Now requires proper Django REST Framework authentication (JWT token)
|
|
|
|
---
|
|
|
|
### 2. Backend Authentication Check Logic
|
|
**Issue:** Weak authentication validation logic that could be bypassed
|
|
|
|
**Location:** `/data/app/igny8/backend/igny8_core/modules/integration/views.py`
|
|
|
|
**Before:**
|
|
```python
|
|
# Weak check with fallbacks
|
|
if not hasattr(request, 'user') or not getattr(request.user, 'is_authenticated', False):
|
|
return error_response('Authentication required', None, status.HTTP_403_FORBIDDEN, request)
|
|
|
|
try:
|
|
if site.account != request.user.account:
|
|
return error_response('Site does not belong to your account', None, status.HTTP_403_FORBIDDEN, request)
|
|
except Exception:
|
|
return error_response('Authentication failed', None, status.HTTP_403_FORBIDDEN, request)
|
|
```
|
|
|
|
**After:**
|
|
```python
|
|
# Strong authentication check
|
|
if not request.user or not request.user.is_authenticated:
|
|
return error_response('Authentication required', None, status.HTTP_401_UNAUTHORIZED, request)
|
|
|
|
if not hasattr(request.user, 'account') or not request.user.account:
|
|
return error_response('User account not found', None, status.HTTP_403_FORBIDDEN, request)
|
|
|
|
if site.account_id != request.user.account_id:
|
|
return error_response('Site does not belong to your account', None, status.HTTP_403_FORBIDDEN, request)
|
|
```
|
|
|
|
**Impact:** Proper 401 for missing auth, clear account validation, uses account_id directly
|
|
|
|
---
|
|
|
|
### 3. WordPress Plugin `/status` Endpoint Security
|
|
**Issue:** Status endpoint was public, returning connection info without authentication
|
|
|
|
**Location:** `/data/app/igny8/plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php`
|
|
|
|
**Before:**
|
|
```php
|
|
register_rest_route('igny8/v1', '/status', array(
|
|
'methods' => 'GET',
|
|
'callback' => array($this, 'get_status'),
|
|
'permission_callback' => '__return_true', // Public endpoint
|
|
));
|
|
```
|
|
|
|
**After:**
|
|
```php
|
|
register_rest_route('igny8/v1', '/status', array(
|
|
'methods' => 'GET',
|
|
'callback' => array($this, 'get_status'),
|
|
'permission_callback' => array($this, 'check_permission'), // Requires API key
|
|
));
|
|
```
|
|
|
|
**Impact:** Status endpoint now requires valid API key in header
|
|
|
|
---
|
|
|
|
### 4. WordPress Plugin Status Response Enhancement
|
|
**Issue:** Status response didn't track API key validation timestamp
|
|
|
|
**Location:** `/data/app/igny8/plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php`
|
|
|
|
**Before:**
|
|
```php
|
|
public function get_status($request) {
|
|
// No validation tracking
|
|
$data = array(
|
|
'connected' => !empty($api_key) && $api->is_authenticated(),
|
|
'has_api_key' => !empty($api_key),
|
|
'last_health_check' => get_option('igny8_last_api_health_check', 0),
|
|
// ...
|
|
);
|
|
}
|
|
```
|
|
|
|
**After:**
|
|
```php
|
|
public function get_status($request) {
|
|
// Update validation timestamp when status is checked with valid API key
|
|
update_option('igny8_last_api_health_check', time());
|
|
|
|
$data = array(
|
|
'connected' => !empty($api_key) && $api->is_authenticated(),
|
|
'has_api_key' => !empty($api_key),
|
|
'api_key_validated' => true, // Since check_permission passed
|
|
'last_health_check' => time(),
|
|
// ...
|
|
);
|
|
}
|
|
```
|
|
|
|
**Impact:** Tracks when API key was last successfully validated
|
|
|
|
---
|
|
|
|
### 5. WordPress Plugin Connection State Logic
|
|
**Issue:** Plugin showed "connected" status without validating API key with backend
|
|
|
|
**Location:** `/data/app/igny8/plugins/wordpress/source/igny8-wp-bridge/includes/functions.php`
|
|
|
|
**Before:**
|
|
```php
|
|
function igny8_get_connection_state() {
|
|
$api_key = igny8_get_secure_option('igny8_api_key');
|
|
$integration_id = get_option('igny8_integration_id');
|
|
$last_structure_sync = get_option('igny8_last_structure_sync');
|
|
|
|
if (empty($api_key)) {
|
|
return 'not_connected';
|
|
}
|
|
|
|
// Shows "connected" if API key exists, even without validation!
|
|
if (!empty($api_key) && !empty($integration_id) && !empty($last_structure_sync)) {
|
|
return 'connected';
|
|
}
|
|
|
|
return 'configured';
|
|
}
|
|
```
|
|
|
|
**After:**
|
|
```php
|
|
function igny8_get_connection_state() {
|
|
$api_key = igny8_get_secure_option('igny8_api_key');
|
|
$integration_id = get_option('igny8_integration_id');
|
|
$last_structure_sync = get_option('igny8_last_structure_sync');
|
|
|
|
// SECURITY: Must have API key to show ANY connection state
|
|
if (empty($api_key)) {
|
|
return 'not_connected';
|
|
}
|
|
|
|
// SECURITY: Only show 'connected' if API key was recently validated
|
|
// Check if we have a recent successful API health check (within last hour)
|
|
$last_health_check = get_option('igny8_last_api_health_check', 0);
|
|
$one_hour_ago = time() - 3600;
|
|
|
|
if (!empty($api_key) && !empty($integration_id) && !empty($last_structure_sync) && $last_health_check > $one_hour_ago) {
|
|
return 'connected';
|
|
}
|
|
|
|
// If we have API key but no recent validation, show as 'configured' not 'connected'
|
|
return 'configured';
|
|
}
|
|
```
|
|
|
|
**Impact:**
|
|
- Only shows "connected" if API key was validated within last hour
|
|
- Shows "configured" if key exists but hasn't been validated recently
|
|
- Prevents false "connected" status on fresh installations
|
|
|
|
---
|
|
|
|
## Security Flow Summary
|
|
|
|
### Before Fixes
|
|
1. ❌ Plugin could show "connected" without API key validation
|
|
2. ❌ Backend `/test-connection/` accepted unauthenticated requests
|
|
3. ❌ Plugin `/status` endpoint was public
|
|
4. ❌ No tracking of when API key was last validated
|
|
|
|
### After Fixes
|
|
1. ✅ Backend `/test-connection/` requires JWT authentication
|
|
2. ✅ Strong authentication checks with proper 401/403 responses
|
|
3. ✅ Plugin `/status` endpoint requires valid API key
|
|
4. ✅ Plugin tracks last successful API key validation
|
|
5. ✅ Connection state only shows "connected" with recent validation
|
|
6. ✅ Clear distinction between "configured" (key exists) and "connected" (key validated)
|
|
|
|
## Authentication Flow
|
|
|
|
### Correct Flow (After Fixes)
|
|
```
|
|
1. User adds API key in WordPress plugin settings
|
|
→ Status: "configured"
|
|
|
|
2. IGNY8 backend calls /wp-json/igny8/v1/verify-key
|
|
→ Plugin validates API key from X-IGNY8-API-KEY header
|
|
→ Updates igny8_last_api_health_check timestamp
|
|
→ Returns 200 OK
|
|
|
|
3. Plugin connection state check
|
|
→ Sees igny8_last_api_health_check < 1 hour old
|
|
→ Status: "connected"
|
|
|
|
4. After 1 hour without health check
|
|
→ Status: returns to "configured"
|
|
→ Next test connection will re-validate and update timestamp
|
|
```
|
|
|
|
## Testing Checklist
|
|
|
|
- [ ] Fresh plugin installation shows "not_connected"
|
|
- [ ] Adding API key shows "configured", NOT "connected"
|
|
- [ ] Test connection with valid API key shows "connected"
|
|
- [ ] Test connection with invalid API key shows error
|
|
- [ ] Backend `/test-connection/` requires authentication (401 without token)
|
|
- [ ] Plugin `/status` endpoint requires API key (401/403 without key)
|
|
- [ ] Connection state returns to "configured" after 1 hour
|
|
- [ ] All API communication properly authenticated
|
|
|
|
## Deployment
|
|
|
|
1. Backend changes applied: ✅ (container restarted)
|
|
2. Plugin changes ready: ✅ (needs deployment to WordPress site)
|
|
|
|
**Next Steps:**
|
|
- Deploy updated plugin to WordPress site (massagersmart.com)
|
|
- Test complete authentication flow
|
|
- Verify no false "connected" states
|
|
- Verify proper error messages for missing/invalid API keys
|
|
|
|
## Files Modified
|
|
|
|
- `backend/igny8_core/modules/integration/views.py`
|
|
- `plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php`
|
|
- `plugins/wordpress/source/igny8-wp-bridge/includes/functions.php`
|
|
|
|
---
|
|
|
|
**Date:** January 13, 2026
|
|
**Status:** Implemented, Backend Deployed, Plugin Ready for Deployment
|
|
**Priority:** CRITICAL - Security Vulnerability Fixes
|