Security fixes: wildcard CORS, JWT auth, zero-address fallback
Phase 1 security remediation from codebase analysis: CORS fixes: - Replace wildcard CORS with safe localhost defaults in agent-coordinator - Replace wildcard CORS with safe localhost defaults in marketplace - Fix 8 additional wildcard CORS instances in coordinator-api apps: - hermes_enhanced_app.py - api_gateway.py - modality_optimization_app.py - multimodal_app.py - gpu_multimodal_app.py - marketplace_enhanced_app.py - advanced_ai_service.py - adaptive_learning_app.py - Add CORS configuration security tests Blockchain-node auth fixes: - JWT authentication now fails closed with clear error message - X-Wallet-Address already gated behind TRUST_X_WALLET_ADDRESS env var - Remove zero-address fallback from arbitration vote submission - Add regression test for zero-address rejection in arbitration Tests: - Update dispute auth tests to reflect new JWT error message - Add test_arbitration_vote_zero_address_rejected - Add test_cors_configuration.py with 5 CORS validation tests
This commit is contained in:
@@ -4,7 +4,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from aitbc.rate_limiting import RateLimitMiddleware
|
||||
|
||||
from .config import settings
|
||||
from .config import settings, validated_cors_origins
|
||||
from .exceptions import register_exception_handlers
|
||||
from .lifespan import lifespan
|
||||
from .middleware import register_middleware
|
||||
@@ -21,7 +21,7 @@ def create_app() -> FastAPI:
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=_validated_cors_origins(settings.cors_origins),
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -10,7 +10,7 @@ import uuid
|
||||
from typing import Any, Dict, Optional, List
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from sqlmodel import select, delete
|
||||
@@ -52,34 +52,32 @@ def get_authenticated_address(request: Request, credentials: Optional[HTTPAuthor
|
||||
# Check for X-Wallet-Address header (API key authentication)
|
||||
wallet_address = request.headers.get("X-Wallet-Address")
|
||||
if wallet_address:
|
||||
# Validate address format (basic check)
|
||||
if not wallet_address.startswith("0x") or len(wallet_address) != 42:
|
||||
_logger.warning(f"Invalid wallet address format in X-Wallet-Address header: {wallet_address}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid wallet address format"
|
||||
)
|
||||
if os.getenv("TRUST_X_WALLET_ADDRESS", "false").lower() != "true":
|
||||
_logger.warning("Rejected untrusted X-Wallet-Address header")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="X-Wallet-Address header is not trusted without explicit server configuration"
|
||||
)
|
||||
_logger.debug(f"Authenticated via X-Wallet-Address header: {wallet_address}")
|
||||
return wallet_address
|
||||
|
||||
# Check for JWT Bearer token
|
||||
if credentials and credentials.scheme == "Bearer":
|
||||
# In a full implementation, this would validate the JWT token
|
||||
# For now, we'll extract a wallet address from the token if present
|
||||
# This is a placeholder for proper JWT validation
|
||||
token = credentials.credentials
|
||||
_logger.debug(f"JWT token provided (validation not yet implemented)")
|
||||
# TODO: Implement proper JWT validation and address extraction
|
||||
# For now, raise an error to require proper implementation
|
||||
_logger.warning("JWT authentication attempted but not supported")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="JWT authentication not yet implemented. Use X-Wallet-Address header."
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="JWT authentication is not supported. Use X-Wallet-Address header with TRUST_X_WALLET_ADDRESS=true for trusted internal requests."
|
||||
)
|
||||
|
||||
# Development mode fallback
|
||||
if os.getenv("DEV_MODE", "false").lower() == "true":
|
||||
_logger.warning("Using development mode fallback for authentication - returning zero address")
|
||||
return "0x0000000000000000000000000000000000000000"
|
||||
_logger.warning("Rejected unauthenticated request in development mode")
|
||||
|
||||
# No valid authentication found
|
||||
raise HTTPException(
|
||||
@@ -1739,17 +1737,22 @@ async def submit_arbitration_vote(
|
||||
try:
|
||||
# Get authenticated address from request
|
||||
arbitrator_address = get_authenticated_address(http_request, credentials)
|
||||
|
||||
# TODO: Implement actual smart contract interaction with arbitrator authorization check
|
||||
# For now, validate that we have a real address (not zero address unless in dev mode)
|
||||
|
||||
# Reject zero address in all modes - this is a sensitive arbitration operation
|
||||
if arbitrator_address == "0x0000000000000000000000000000000000000000":
|
||||
_logger.warning("Vote submission attempted with zero address - may be in dev mode")
|
||||
|
||||
_logger.error("Vote submission attempted with zero address - rejected")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Zero address is not allowed for arbitration operations"
|
||||
)
|
||||
|
||||
return SubmitArbitrationVoteResponse(
|
||||
success=True,
|
||||
status="Submitted",
|
||||
message=f"Vote submitted successfully for dispute {request.dispute_id}"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
_logger.error(f"Error submitting arbitration vote: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to submit vote: {str(e)}")
|
||||
|
||||
@@ -22,7 +22,16 @@ app = FastAPI(
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[
|
||||
"http://localhost:8001",
|
||||
"http://localhost:8011",
|
||||
"http://localhost:8016",
|
||||
"http://localhost:9001",
|
||||
"http://127.0.0.1:8001",
|
||||
"http://127.0.0.1:8011",
|
||||
"http://127.0.0.1:8016",
|
||||
"http://127.0.0.1:9001",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -20,7 +20,16 @@ app = FastAPI(
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[
|
||||
"http://localhost:8001",
|
||||
"http://localhost:8011",
|
||||
"http://localhost:8016",
|
||||
"http://localhost:9001",
|
||||
"http://127.0.0.1:8001",
|
||||
"http://127.0.0.1:8011",
|
||||
"http://127.0.0.1:8016",
|
||||
"http://127.0.0.1:9001",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -21,7 +21,16 @@ app = FastAPI(
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[
|
||||
"http://localhost:8001",
|
||||
"http://localhost:8011",
|
||||
"http://localhost:8016",
|
||||
"http://localhost:9001",
|
||||
"http://127.0.0.1:8001",
|
||||
"http://127.0.0.1:8011",
|
||||
"http://127.0.0.1:8016",
|
||||
"http://127.0.0.1:9001",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -74,7 +74,16 @@ app = FastAPI(
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[
|
||||
"http://localhost:8001",
|
||||
"http://localhost:8011",
|
||||
"http://localhost:8016",
|
||||
"http://localhost:9001",
|
||||
"http://127.0.0.1:8001",
|
||||
"http://127.0.0.1:8011",
|
||||
"http://127.0.0.1:8016",
|
||||
"http://127.0.0.1:9001",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -466,7 +466,16 @@ app = FastAPI(
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[
|
||||
"http://localhost:8001",
|
||||
"http://localhost:8011",
|
||||
"http://localhost:8016",
|
||||
"http://localhost:9001",
|
||||
"http://127.0.0.1:8001",
|
||||
"http://127.0.0.1:8011",
|
||||
"http://127.0.0.1:8016",
|
||||
"http://127.0.0.1:9001",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -21,7 +21,16 @@ app = FastAPI(
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[
|
||||
"http://localhost:8001",
|
||||
"http://localhost:8011",
|
||||
"http://localhost:8016",
|
||||
"http://localhost:9001",
|
||||
"http://127.0.0.1:8001",
|
||||
"http://127.0.0.1:8011",
|
||||
"http://127.0.0.1:8016",
|
||||
"http://127.0.0.1:9001",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -21,7 +21,16 @@ app = FastAPI(
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[
|
||||
"http://localhost:8001",
|
||||
"http://localhost:8011",
|
||||
"http://localhost:8016",
|
||||
"http://localhost:9001",
|
||||
"http://127.0.0.1:8001",
|
||||
"http://127.0.0.1:8011",
|
||||
"http://127.0.0.1:8016",
|
||||
"http://127.0.0.1:9001",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -21,7 +21,16 @@ app = FastAPI(
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=[
|
||||
"http://localhost:8001",
|
||||
"http://localhost:8011",
|
||||
"http://localhost:8016",
|
||||
"http://localhost:9001",
|
||||
"http://127.0.0.1:8001",
|
||||
"http://127.0.0.1:8011",
|
||||
"http://127.0.0.1:8016",
|
||||
"http://127.0.0.1:9001",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -5,6 +5,7 @@ Miners register GPU offerings, choose chains, and confirm deals
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
@@ -14,6 +15,28 @@ from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
import uvicorn
|
||||
|
||||
DEFAULT_CORS_ORIGINS = [
|
||||
"http://localhost:8001",
|
||||
"http://localhost:8011",
|
||||
"http://localhost:8016",
|
||||
"http://localhost:9001",
|
||||
"http://127.0.0.1:8001",
|
||||
"http://127.0.0.1:8011",
|
||||
"http://127.0.0.1:8016",
|
||||
"http://127.0.0.1:9001",
|
||||
]
|
||||
|
||||
|
||||
def get_cors_origins() -> List[str]:
|
||||
raw_origins = os.getenv("AITBC_MARKETPLACE_CORS_ORIGINS")
|
||||
if not raw_origins:
|
||||
return DEFAULT_CORS_ORIGINS
|
||||
origins = [origin.strip() for origin in raw_origins.split(",") if origin.strip()]
|
||||
if "*" in origins:
|
||||
raise ValueError("Wildcard CORS origins are not allowed when credentials are enabled")
|
||||
return origins
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="AITBC Agent-First GPU Marketplace",
|
||||
description="GPU trading marketplace where miners register offerings and confirm deals",
|
||||
@@ -23,7 +46,7 @@ app = FastAPI(
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=get_cors_origins(),
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -5,18 +5,26 @@ Tests for missing authentication, unauthorized access, and invalid tokens.
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest_asyncio
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
from fastapi import status
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
blockchain_src = repo_root / "apps" / "blockchain-node" / "src"
|
||||
if str(blockchain_src) not in sys.path:
|
||||
sys.path.insert(0, str(blockchain_src))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDisputeAuthentication:
|
||||
"""Test authentication requirements for dispute endpoints"""
|
||||
|
||||
@pytest.fixture
|
||||
@pytest_asyncio.fixture
|
||||
async def client(self):
|
||||
"""Create test client for blockchain node RPC"""
|
||||
from apps.blockchain_node.src.aitbc_chain.rpc.router import router
|
||||
from aitbc_chain.rpc.router import router
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
@@ -24,7 +32,9 @@ class TestDisputeAuthentication:
|
||||
|
||||
# Set DEV_MODE to false for production-like testing
|
||||
original_dev_mode = os.getenv("DEV_MODE")
|
||||
original_trust_header = os.getenv("TRUST_X_WALLET_ADDRESS")
|
||||
os.environ["DEV_MODE"] = "false"
|
||||
os.environ["TRUST_X_WALLET_ADDRESS"] = "false"
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
@@ -41,23 +51,23 @@ class TestDisputeAuthentication:
|
||||
response = await client.post(
|
||||
"/rpc/disputes/file",
|
||||
json={
|
||||
"agreement_id": "test_agreement_1",
|
||||
"agreement_id": 1,
|
||||
"respondent": "0x1234567890123456789012345678901234567890",
|
||||
"dispute_type": "payment_dispute",
|
||||
"reason": "Test dispute",
|
||||
"evidence_hash": "0xabcdef"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert "Authentication required" in response.json()["detail"]
|
||||
|
||||
|
||||
async def test_file_dispute_with_invalid_wallet_address(self, client):
|
||||
"""Test that filing a dispute with invalid wallet address format returns 401"""
|
||||
response = await client.post(
|
||||
"/rpc/disputes/file",
|
||||
json={
|
||||
"agreement_id": "test_agreement_1",
|
||||
"agreement_id": 1,
|
||||
"respondent": "0x1234567890123456789012345678901234567890",
|
||||
"dispute_type": "payment_dispute",
|
||||
"reason": "Test dispute",
|
||||
@@ -65,10 +75,10 @@ class TestDisputeAuthentication:
|
||||
},
|
||||
headers={"X-Wallet-Address": "invalid_address_format"}
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert "Invalid wallet address format" in response.json()["detail"]
|
||||
|
||||
|
||||
async def test_submit_evidence_missing_auth(self, client):
|
||||
"""Test that submitting evidence without authentication returns 401"""
|
||||
response = await client.post(
|
||||
@@ -79,10 +89,10 @@ class TestDisputeAuthentication:
|
||||
"evidence_data": "test_evidence_data"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert "Authentication required" in response.json()["detail"]
|
||||
|
||||
|
||||
async def test_verify_evidence_missing_auth(self, client):
|
||||
"""Test that verifying evidence without authentication returns 401"""
|
||||
response = await client.post(
|
||||
@@ -94,10 +104,10 @@ class TestDisputeAuthentication:
|
||||
"verification_score": 95
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert "Authentication required" in response.json()["detail"]
|
||||
|
||||
|
||||
async def test_authorize_arbitrator_missing_auth(self, client):
|
||||
"""Test that authorizing an arbitrator without authentication returns 401"""
|
||||
response = await client.post(
|
||||
@@ -107,10 +117,10 @@ class TestDisputeAuthentication:
|
||||
"reputation_score": 85
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert "Authentication required" in response.json()["detail"]
|
||||
|
||||
|
||||
async def test_submit_vote_missing_auth(self, client):
|
||||
"""Test that submitting a vote without authentication returns 401"""
|
||||
response = await client.post(
|
||||
@@ -122,16 +132,17 @@ class TestDisputeAuthentication:
|
||||
"reasoning": "Test reasoning"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert "Authentication required" in response.json()["detail"]
|
||||
|
||||
|
||||
async def test_file_dispute_with_valid_wallet_address(self, client):
|
||||
"""Test that filing a dispute with valid wallet address header succeeds (or returns expected error)"""
|
||||
os.environ["TRUST_X_WALLET_ADDRESS"] = "true"
|
||||
response = await client.post(
|
||||
"/rpc/disputes/file",
|
||||
json={
|
||||
"agreement_id": "test_agreement_1",
|
||||
"agreement_id": 1,
|
||||
"respondent": "0x1234567890123456789012345678901234567890",
|
||||
"dispute_type": "payment_dispute",
|
||||
"reason": "Test dispute",
|
||||
@@ -139,17 +150,34 @@ class TestDisputeAuthentication:
|
||||
},
|
||||
headers={"X-Wallet-Address": "0x1234567890123456789012345678901234567890"}
|
||||
)
|
||||
|
||||
|
||||
# Should not be 401 (authentication passed)
|
||||
# May be 500 if dispute service is not available, which is acceptable
|
||||
assert response.status_code != status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
async def test_jwt_token_not_implemented(self, client):
|
||||
"""Test that JWT token authentication returns 501 (not yet implemented)"""
|
||||
|
||||
async def test_valid_wallet_address_header_is_not_trusted_by_default(self, client):
|
||||
"""Test that filing a dispute with an untrusted wallet address header returns 401"""
|
||||
response = await client.post(
|
||||
"/rpc/disputes/file",
|
||||
json={
|
||||
"agreement_id": "test_agreement_1",
|
||||
"agreement_id": 1,
|
||||
"respondent": "0x1234567890123456789012345678901234567890",
|
||||
"dispute_type": "payment_dispute",
|
||||
"reason": "Test dispute",
|
||||
"evidence_hash": "0xabcdef"
|
||||
},
|
||||
headers={"X-Wallet-Address": "0x1234567890123456789012345678901234567890"}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert "not trusted" in response.json()["detail"]
|
||||
|
||||
async def test_jwt_token_not_configured(self, client):
|
||||
"""Test that JWT token authentication fails closed with clear error"""
|
||||
response = await client.post(
|
||||
"/rpc/disputes/file",
|
||||
json={
|
||||
"agreement_id": 1,
|
||||
"respondent": "0x1234567890123456789012345678901234567890",
|
||||
"dispute_type": "payment_dispute",
|
||||
"reason": "Test dispute",
|
||||
@@ -157,19 +185,19 @@ class TestDisputeAuthentication:
|
||||
},
|
||||
headers={"Authorization": "Bearer test_token"}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED
|
||||
assert "JWT authentication not yet implemented" in response.json()["detail"]
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert "JWT authentication is not supported" in response.json()["detail"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDisputeAuthDevMode:
|
||||
"""Test authentication behavior in development mode"""
|
||||
|
||||
@pytest.fixture
|
||||
@pytest_asyncio.fixture
|
||||
async def dev_client(self):
|
||||
"""Create test client with DEV_MODE enabled"""
|
||||
from apps.blockchain_node.src.aitbc_chain.rpc.router import router
|
||||
from aitbc_chain.rpc.router import router
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
@@ -177,7 +205,9 @@ class TestDisputeAuthDevMode:
|
||||
|
||||
# Set DEV_MODE to true
|
||||
original_dev_mode = os.getenv("DEV_MODE")
|
||||
original_trust_header = os.getenv("TRUST_X_WALLET_ADDRESS")
|
||||
os.environ["DEV_MODE"] = "true"
|
||||
os.environ["TRUST_X_WALLET_ADDRESS"] = "false"
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
@@ -189,19 +219,35 @@ class TestDisputeAuthDevMode:
|
||||
else:
|
||||
os.environ["DEV_MODE"] = original_dev_mode
|
||||
|
||||
async def test_file_dispute_dev_mode_fallback(self, dev_client):
|
||||
"""Test that in dev mode, missing auth uses zero address fallback"""
|
||||
async def test_file_dispute_dev_mode_fails_closed(self, dev_client):
|
||||
"""Test that dev mode no longer uses a zero address fallback"""
|
||||
response = await dev_client.post(
|
||||
"/rpc/disputes/file",
|
||||
json={
|
||||
"agreement_id": "test_agreement_1",
|
||||
"agreement_id": 1,
|
||||
"respondent": "0x1234567890123456789012345678901234567890",
|
||||
"dispute_type": "payment_dispute",
|
||||
"reason": "Test dispute",
|
||||
"evidence_hash": "0xabcdef"
|
||||
}
|
||||
)
|
||||
|
||||
# In dev mode, should not return 401 (uses zero address fallback)
|
||||
# May return 500 if dispute service is not available
|
||||
assert response.status_code != status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert "Authentication required" in response.json()["detail"]
|
||||
|
||||
async def test_arbitration_vote_zero_address_rejected(self, dev_client):
|
||||
"""Test that zero address is rejected in arbitration vote submission"""
|
||||
os.environ["TRUST_X_WALLET_ADDRESS"] = "true"
|
||||
response = await dev_client.post(
|
||||
"/rpc/disputes/vote",
|
||||
json={
|
||||
"dispute_id": 1,
|
||||
"vote_in_favor_of_initiator": True,
|
||||
"confidence": 90,
|
||||
"reasoning": "Test reasoning"
|
||||
},
|
||||
headers={"X-Wallet-Address": "0x0000000000000000000000000000000000000000"}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert "Zero address is not allowed" in response.json()["detail"]
|
||||
|
||||
135
tests/security/test_cors_configuration.py
Normal file
135
tests/security/test_cors_configuration.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
CORS configuration security tests.
|
||||
Validates that wildcard CORS is not used with allow_credentials=True.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_agent_coordinator_cors_rejects_wildcard():
|
||||
"""Test that agent-coordinator config rejects wildcard origins"""
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
agent_coordinator_src = repo_root / "apps" / "agent-coordinator" / "src"
|
||||
|
||||
if str(agent_coordinator_src) not in sys.path:
|
||||
sys.path.insert(0, str(agent_coordinator_src))
|
||||
|
||||
# Set required secret_key to avoid validation error
|
||||
os.environ["SECRET_KEY"] = "test_secret_key_for_testing"
|
||||
|
||||
from app.config import validated_cors_origins
|
||||
|
||||
with pytest.raises(ValueError, match="Wildcard CORS origins are not allowed"):
|
||||
validated_cors_origins(["*"])
|
||||
|
||||
# Clean up
|
||||
os.environ.pop("SECRET_KEY", None)
|
||||
|
||||
|
||||
def test_agent_coordinator_cors_accepts_localhost():
|
||||
"""Test that agent-coordinator config accepts localhost origins"""
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
agent_coordinator_src = repo_root / "apps" / "agent-coordinator" / "src"
|
||||
|
||||
if str(agent_coordinator_src) not in sys.path:
|
||||
sys.path.insert(0, str(agent_coordinator_src))
|
||||
|
||||
# Set required secret_key to avoid validation error
|
||||
os.environ["SECRET_KEY"] = "test_secret_key_for_testing"
|
||||
|
||||
from app.config import validated_cors_origins
|
||||
|
||||
origins = [
|
||||
"http://localhost:8001",
|
||||
"http://localhost:9001",
|
||||
"http://127.0.0.1:8001",
|
||||
]
|
||||
result = validated_cors_origins(origins)
|
||||
assert result == origins
|
||||
|
||||
# Clean up
|
||||
os.environ.pop("SECRET_KEY", None)
|
||||
|
||||
|
||||
def test_marketplace_cors_rejects_wildcard():
|
||||
"""Test that marketplace rejects wildcard origins via environment variable"""
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
marketplace_src = repo_root / "apps" / "marketplace"
|
||||
|
||||
if str(marketplace_src) not in sys.path:
|
||||
sys.path.insert(0, str(marketplace_src))
|
||||
|
||||
# Set environment variable with wildcard
|
||||
os.environ["AITBC_MARKETPLACE_CORS_ORIGINS"] = "*"
|
||||
|
||||
# The marketplace module raises ValueError on import when wildcard is set
|
||||
# This is the expected behavior
|
||||
with pytest.raises(ValueError, match="Wildcard CORS origins are not allowed"):
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("agent_marketplace", marketplace_src / "agent_marketplace.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Clean up
|
||||
os.environ.pop("AITBC_MARKETPLACE_CORS_ORIGINS", None)
|
||||
|
||||
|
||||
def test_marketplace_cors_accepts_localhost():
|
||||
"""Test that marketplace accepts localhost origins via environment variable"""
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
marketplace_src = repo_root / "apps" / "marketplace"
|
||||
|
||||
if str(marketplace_src) not in sys.path:
|
||||
sys.path.insert(0, str(marketplace_src))
|
||||
|
||||
os.environ["AITBC_MARKETPLACE_CORS_ORIGINS"] = "http://localhost:8001,http://localhost:9001"
|
||||
|
||||
# Import the function directly from the file
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("agent_marketplace", marketplace_src / "agent_marketplace.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
result = module.get_cors_origins()
|
||||
assert "http://localhost:8001" in result
|
||||
assert "http://localhost:9001" in result
|
||||
|
||||
# Clean up
|
||||
os.environ.pop("AITBC_MARKETPLACE_CORS_ORIGINS", None)
|
||||
|
||||
|
||||
def test_no_wildcard_cors_in_coordinator_api_apps():
|
||||
"""Scan coordinator-api apps for wildcard CORS with credentials"""
|
||||
import re
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
coordinator_src = repo_root / "apps" / "coordinator-api" / "src"
|
||||
|
||||
files_to_check = [
|
||||
coordinator_src / "app" / "contexts" / "hermes" / "routers" / "hermes_enhanced_app.py",
|
||||
coordinator_src / "app" / "services" / "enterprise_integration" / "api_gateway.py",
|
||||
coordinator_src / "app" / "services" / "modality_optimization_app.py",
|
||||
coordinator_src / "app" / "services" / "multimodal_app.py",
|
||||
coordinator_src / "app" / "services" / "gpu_multimodal_app.py",
|
||||
coordinator_src / "app" / "routers" / "marketplace_enhanced_app.py",
|
||||
coordinator_src / "app" / "services" / "advanced_ai_service.py",
|
||||
coordinator_src / "app" / "services" / "adaptive_learning_app.py",
|
||||
]
|
||||
|
||||
wildcard_pattern = re.compile(r'allow_origins\s*=\s*\["\*"\]')
|
||||
credentials_pattern = re.compile(r'allow_credentials\s*=\s*True')
|
||||
|
||||
for file_path in files_to_check:
|
||||
if not file_path.exists():
|
||||
continue
|
||||
|
||||
content = file_path.read_text()
|
||||
has_wildcard = wildcard_pattern.search(content) is not None
|
||||
has_credentials = credentials_pattern.search(content) is not None
|
||||
|
||||
# If both wildcard and credentials are present, fail the test
|
||||
if has_wildcard and has_credentials:
|
||||
pytest.fail(f"File {file_path} contains wildcard CORS with credentials enabled")
|
||||
Reference in New Issue
Block a user