mirror of
https://github.com/apidoorman/doorman.git
synced 2026-02-09 11:07:05 -06:00
233 lines
8.9 KiB
Python
233 lines
8.9 KiB
Python
"""
|
|
Test Cache Invalidation on Database Failures
|
|
Documents the implementation of cache invalidation to prevent stale data causing auth bypass
|
|
"""
|
|
|
|
def test_cache_invalidation_patterns():
|
|
"""Test cache invalidation patterns on DB operations"""
|
|
|
|
print("Cache Invalidation on Database Failures - Implementation")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("P0 Security Risk:")
|
|
print(" Cache not invalidated when DB updates fail")
|
|
print(" → Stale user data in cache causes auth bypass")
|
|
print(" → Example: Revoked user still cached as active")
|
|
print(" → Example: Role change not reflected in cache")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Implementation Locations:")
|
|
print()
|
|
|
|
implementations = [
|
|
{
|
|
'file': 'utils/doorman_cache_util.py',
|
|
'location': 'lines 217-250',
|
|
'change': 'Added invalidate_on_db_failure() helper method',
|
|
'purpose': 'Wrapper for DB operations with automatic cache invalidation'
|
|
},
|
|
{
|
|
'file': 'services/user_service.py',
|
|
'location': 'lines 279-293 (update_user)',
|
|
'change': 'Wrapped update_one with try/except, invalidate on success and failure',
|
|
'purpose': 'Prevent stale user cache after update failures'
|
|
},
|
|
{
|
|
'file': 'services/user_service.py',
|
|
'location': 'lines 360-367 (update_password)',
|
|
'change': 'Wrapped update_one with try/except, invalidate on success and failure',
|
|
'purpose': 'Prevent stale user cache after password update failures'
|
|
},
|
|
{
|
|
'file': 'services/user_service.py',
|
|
'location': 'lines 407-418 (purge_apis_after_role_change)',
|
|
'change': 'Wrapped update_one with try/except, invalidate on failure',
|
|
'purpose': 'Prevent stale subscription cache after update failures'
|
|
},
|
|
{
|
|
'file': 'services/role_service.py',
|
|
'location': 'lines 99-117 (update_role)',
|
|
'change': 'Wrapped update_one with try/except, invalidate on success and failure',
|
|
'purpose': 'Prevent stale role cache after update failures'
|
|
},
|
|
{
|
|
'file': 'services/api_service.py',
|
|
'location': 'lines 128-149 (update_api)',
|
|
'change': 'Wrapped update_one with try/except, invalidate on success and failure',
|
|
'purpose': 'Prevent stale API cache after update failures'
|
|
}
|
|
]
|
|
|
|
for i, impl in enumerate(implementations, 1):
|
|
print(f"{i}. {impl['file']}")
|
|
print(f" Location: {impl['location']}")
|
|
print(f" Change: {impl['change']}")
|
|
print(f" Purpose: {impl['purpose']}")
|
|
print()
|
|
|
|
print("=" * 70)
|
|
print()
|
|
print("Cache Invalidation Strategy:")
|
|
print()
|
|
print("BEFORE (Vulnerable):")
|
|
print(" try:")
|
|
print(" result = user_collection.update_one(...)")
|
|
print(" except Exception:")
|
|
print(" pass # Cache still has stale data!")
|
|
print()
|
|
print("Problems:")
|
|
print(" - DB update fails, cache not invalidated")
|
|
print(" - Next request gets stale cached data")
|
|
print(" - Revoked users can still authenticate")
|
|
print(" - Role changes not reflected in auth")
|
|
print()
|
|
print("AFTER (Secure):")
|
|
print(" try:")
|
|
print(" result = user_collection.update_one(...)")
|
|
print(" if result.modified_count > 0:")
|
|
print(" doorman_cache.delete_cache('user_cache', username)")
|
|
print(" except Exception as e:")
|
|
print(" doorman_cache.delete_cache('user_cache', username)")
|
|
print(" logger.error(f'Update failed: {e}', exc_info=True)")
|
|
print(" raise")
|
|
print()
|
|
print("Benefits:")
|
|
print(" ✓ Cache invalidated on successful updates")
|
|
print(" ✓ Cache invalidated on DB exceptions (forces fresh read)")
|
|
print(" ✓ Prevents stale data from cached reads")
|
|
print(" ✓ Auth bypass prevented")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
print("Invalidation Triggers:")
|
|
print()
|
|
print("1. Successful Update (modified_count > 0)")
|
|
print(" - User updated → invalidate user_cache")
|
|
print(" - Role updated → invalidate role_cache")
|
|
print(" - API updated → invalidate api_cache + api_id_cache")
|
|
print()
|
|
print("2. Database Exception")
|
|
print(" - Connection failure → invalidate cache")
|
|
print(" - Write timeout → invalidate cache")
|
|
print(" - Any DB error → invalidate cache")
|
|
print(" - Reason: Force fresh DB read on next access")
|
|
print()
|
|
print("3. NOT Invalidated:")
|
|
print(" - No matching document (modified_count == 0, no exception)")
|
|
print(" - Reason: Cache might not exist, no need to invalidate")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
print("Security Impact:")
|
|
print()
|
|
print("Attack Scenario BEFORE Fix:")
|
|
print(" 1. Attacker's user account is disabled (DB updated)")
|
|
print(" 2. DB write fails silently due to network partition")
|
|
print(" 3. Cache still has 'active' user data")
|
|
print(" 4. Attacker authenticates successfully (uses stale cache)")
|
|
print(" 5. → Auth bypass due to stale cache")
|
|
print()
|
|
print("Attack Scenario AFTER Fix:")
|
|
print(" 1. Attacker's user account is disabled (DB update attempted)")
|
|
print(" 2. DB write fails due to network partition")
|
|
print(" 3. Cache invalidated on exception")
|
|
print(" 4. Next auth attempt reads fresh from DB")
|
|
print(" 5. → Auth correctly fails (no stale cache)")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
print("Coverage:")
|
|
print()
|
|
|
|
coverage_areas = [
|
|
{
|
|
'entity': 'User Cache',
|
|
'operations': ['update_user', 'update_password', 'delete_user'],
|
|
'cache_keys': ['user_cache:{username}', 'user_subscription_cache:{username}']
|
|
},
|
|
{
|
|
'entity': 'Role Cache',
|
|
'operations': ['update_role', 'delete_role'],
|
|
'cache_keys': ['role_cache:{role_name}']
|
|
},
|
|
{
|
|
'entity': 'API Cache',
|
|
'operations': ['update_api', 'delete_api'],
|
|
'cache_keys': ['api_cache:{api_name}/{api_version}', 'api_id_cache:/{api_name}/{api_version}']
|
|
}
|
|
]
|
|
|
|
for area in coverage_areas:
|
|
print(f"{area['entity']}:")
|
|
print(f" Operations: {', '.join(area['operations'])}")
|
|
print(f" Cache Keys: {', '.join(area['cache_keys'])}")
|
|
print()
|
|
|
|
print("=" * 70)
|
|
print()
|
|
print("Helper Method: invalidate_on_db_failure()")
|
|
print()
|
|
print("Location: utils/doorman_cache_util.py:217-250")
|
|
print()
|
|
print("Signature:")
|
|
print(" def invalidate_on_db_failure(self, cache_name, key, operation):")
|
|
print()
|
|
print("Usage Example:")
|
|
print(" try:")
|
|
print(" result = user_collection.update_one(...)")
|
|
print(" doorman_cache.invalidate_on_db_failure(")
|
|
print(" 'user_cache', username, lambda: result")
|
|
print(" )")
|
|
print(" except Exception as e:")
|
|
print(" doorman_cache.delete_cache('user_cache', username)")
|
|
print(" raise")
|
|
print()
|
|
print("Behavior:")
|
|
print(" - Checks result.modified_count > 0 → invalidate")
|
|
print(" - Checks result.deleted_count > 0 → invalidate")
|
|
print(" - On exception → invalidate and re-raise")
|
|
print()
|
|
print("Note: Currently implemented inline in services rather than using helper")
|
|
print(" Inline implementation provides more explicit control and logging")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
print("Testing Recommendations:")
|
|
print()
|
|
print("1. Simulate DB Failure:")
|
|
print(" - Mock MongoDB connection to raise exception")
|
|
print(" - Verify cache invalidated after exception")
|
|
print(" - Verify next read goes to DB (not cache)")
|
|
print()
|
|
print("2. Test Successful Update:")
|
|
print(" - Update user with modified_count > 0")
|
|
print(" - Verify cache invalidated")
|
|
print(" - Verify next read gets fresh data")
|
|
print()
|
|
print("3. Test No-Op Update:")
|
|
print(" - Update user with modified_count == 0 (no changes)")
|
|
print(" - Verify cache NOT invalidated (no need)")
|
|
print()
|
|
print("4. Integration Test:")
|
|
print(" - Update user role → verify role_cache invalidated")
|
|
print(" - Password update → verify user_cache invalidated")
|
|
print(" - API update → verify api_cache + api_id_cache invalidated")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
print("P0 Risk Mitigated:")
|
|
print(" Cache not invalidated on database failures causing auth bypass")
|
|
print()
|
|
print("Production Impact:")
|
|
print(" ✓ Revoked users cannot use stale cache to authenticate")
|
|
print(" ✓ Role changes immediately reflected (no stale cache)")
|
|
print(" ✓ API configuration changes take effect immediately")
|
|
print(" ✓ Database failures don't leave stale cached data")
|
|
print()
|
|
|
|
if __name__ == '__main__':
|
|
test_cache_invalidation_patterns()
|