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 aitbc.rate_limiting import RateLimitMiddleware
|
||||||
|
|
||||||
from .config import settings
|
from .config import settings, validated_cors_origins
|
||||||
from .exceptions import register_exception_handlers
|
from .exceptions import register_exception_handlers
|
||||||
from .lifespan import lifespan
|
from .lifespan import lifespan
|
||||||
from .middleware import register_middleware
|
from .middleware import register_middleware
|
||||||
@@ -21,7 +21,7 @@ def create_app() -> FastAPI:
|
|||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=_validated_cors_origins(settings.cors_origins),
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import uuid
|
|||||||
from typing import Any, Dict, Optional, List
|
from typing import Any, Dict, Optional, List
|
||||||
from datetime import datetime, timezone, timedelta
|
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 fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field, model_validator
|
||||||
from sqlmodel import select, delete
|
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)
|
# Check for X-Wallet-Address header (API key authentication)
|
||||||
wallet_address = request.headers.get("X-Wallet-Address")
|
wallet_address = request.headers.get("X-Wallet-Address")
|
||||||
if wallet_address:
|
if wallet_address:
|
||||||
# Validate address format (basic check)
|
|
||||||
if not wallet_address.startswith("0x") or len(wallet_address) != 42:
|
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}")
|
_logger.warning(f"Invalid wallet address format in X-Wallet-Address header: {wallet_address}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid wallet address format"
|
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}")
|
_logger.debug(f"Authenticated via X-Wallet-Address header: {wallet_address}")
|
||||||
return wallet_address
|
return wallet_address
|
||||||
|
|
||||||
# Check for JWT Bearer token
|
# Check for JWT Bearer token
|
||||||
if credentials and credentials.scheme == "Bearer":
|
if credentials and credentials.scheme == "Bearer":
|
||||||
# In a full implementation, this would validate the JWT token
|
_logger.warning("JWT authentication attempted but not supported")
|
||||||
# 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
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="JWT authentication not yet implemented. Use X-Wallet-Address header."
|
detail="JWT authentication is not supported. Use X-Wallet-Address header with TRUST_X_WALLET_ADDRESS=true for trusted internal requests."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Development mode fallback
|
# Development mode fallback
|
||||||
if os.getenv("DEV_MODE", "false").lower() == "true":
|
if os.getenv("DEV_MODE", "false").lower() == "true":
|
||||||
_logger.warning("Using development mode fallback for authentication - returning zero address")
|
_logger.warning("Rejected unauthenticated request in development mode")
|
||||||
return "0x0000000000000000000000000000000000000000"
|
|
||||||
|
|
||||||
# No valid authentication found
|
# No valid authentication found
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -1740,16 +1738,21 @@ async def submit_arbitration_vote(
|
|||||||
# Get authenticated address from request
|
# Get authenticated address from request
|
||||||
arbitrator_address = get_authenticated_address(http_request, credentials)
|
arbitrator_address = get_authenticated_address(http_request, credentials)
|
||||||
|
|
||||||
# TODO: Implement actual smart contract interaction with arbitrator authorization check
|
# Reject zero address in all modes - this is a sensitive arbitration operation
|
||||||
# For now, validate that we have a real address (not zero address unless in dev mode)
|
|
||||||
if arbitrator_address == "0x0000000000000000000000000000000000000000":
|
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(
|
return SubmitArbitrationVoteResponse(
|
||||||
success=True,
|
success=True,
|
||||||
status="Submitted",
|
status="Submitted",
|
||||||
message=f"Vote submitted successfully for dispute {request.dispute_id}"
|
message=f"Vote submitted successfully for dispute {request.dispute_id}"
|
||||||
)
|
)
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.error(f"Error submitting arbitration vote: {e}")
|
_logger.error(f"Error submitting arbitration vote: {e}")
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to submit vote: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Failed to submit vote: {str(e)}")
|
||||||
|
|||||||
@@ -22,7 +22,16 @@ app = FastAPI(
|
|||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
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_credentials=True,
|
||||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@@ -20,7 +20,16 @@ app = FastAPI(
|
|||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
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_credentials=True,
|
||||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@@ -21,7 +21,16 @@ app = FastAPI(
|
|||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
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_credentials=True,
|
||||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@@ -74,7 +74,16 @@ app = FastAPI(
|
|||||||
# CORS middleware
|
# CORS middleware
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
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_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@@ -466,7 +466,16 @@ app = FastAPI(
|
|||||||
# CORS middleware
|
# CORS middleware
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
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_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@@ -21,7 +21,16 @@ app = FastAPI(
|
|||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
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_credentials=True,
|
||||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@@ -21,7 +21,16 @@ app = FastAPI(
|
|||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
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_credentials=True,
|
||||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@@ -21,7 +21,16 @@ app = FastAPI(
|
|||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
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_credentials=True,
|
||||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Miners register GPU offerings, choose chains, and confirm deals
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, List, Any, Optional
|
from typing import Dict, List, Any, Optional
|
||||||
@@ -14,6 +15,28 @@ from fastapi.responses import JSONResponse
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import uvicorn
|
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(
|
app = FastAPI(
|
||||||
title="AITBC Agent-First GPU Marketplace",
|
title="AITBC Agent-First GPU Marketplace",
|
||||||
description="GPU trading marketplace where miners register offerings and confirm deals",
|
description="GPU trading marketplace where miners register offerings and confirm deals",
|
||||||
@@ -23,7 +46,7 @@ app = FastAPI(
|
|||||||
# Add CORS middleware
|
# Add CORS middleware
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=get_cors_origins(),
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|||||||
@@ -5,18 +5,26 @@ Tests for missing authentication, unauthorized access, and invalid tokens.
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
import pytest_asyncio
|
||||||
from httpx import AsyncClient, ASGITransport
|
from httpx import AsyncClient, ASGITransport
|
||||||
from fastapi import status
|
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
|
@pytest.mark.asyncio
|
||||||
class TestDisputeAuthentication:
|
class TestDisputeAuthentication:
|
||||||
"""Test authentication requirements for dispute endpoints"""
|
"""Test authentication requirements for dispute endpoints"""
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest_asyncio.fixture
|
||||||
async def client(self):
|
async def client(self):
|
||||||
"""Create test client for blockchain node RPC"""
|
"""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
|
from fastapi import FastAPI
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -24,7 +32,9 @@ class TestDisputeAuthentication:
|
|||||||
|
|
||||||
# Set DEV_MODE to false for production-like testing
|
# Set DEV_MODE to false for production-like testing
|
||||||
original_dev_mode = os.getenv("DEV_MODE")
|
original_dev_mode = os.getenv("DEV_MODE")
|
||||||
|
original_trust_header = os.getenv("TRUST_X_WALLET_ADDRESS")
|
||||||
os.environ["DEV_MODE"] = "false"
|
os.environ["DEV_MODE"] = "false"
|
||||||
|
os.environ["TRUST_X_WALLET_ADDRESS"] = "false"
|
||||||
|
|
||||||
transport = ASGITransport(app=app)
|
transport = ASGITransport(app=app)
|
||||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||||
@@ -41,7 +51,7 @@ class TestDisputeAuthentication:
|
|||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/rpc/disputes/file",
|
"/rpc/disputes/file",
|
||||||
json={
|
json={
|
||||||
"agreement_id": "test_agreement_1",
|
"agreement_id": 1,
|
||||||
"respondent": "0x1234567890123456789012345678901234567890",
|
"respondent": "0x1234567890123456789012345678901234567890",
|
||||||
"dispute_type": "payment_dispute",
|
"dispute_type": "payment_dispute",
|
||||||
"reason": "Test dispute",
|
"reason": "Test dispute",
|
||||||
@@ -57,7 +67,7 @@ class TestDisputeAuthentication:
|
|||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/rpc/disputes/file",
|
"/rpc/disputes/file",
|
||||||
json={
|
json={
|
||||||
"agreement_id": "test_agreement_1",
|
"agreement_id": 1,
|
||||||
"respondent": "0x1234567890123456789012345678901234567890",
|
"respondent": "0x1234567890123456789012345678901234567890",
|
||||||
"dispute_type": "payment_dispute",
|
"dispute_type": "payment_dispute",
|
||||||
"reason": "Test dispute",
|
"reason": "Test dispute",
|
||||||
@@ -128,10 +138,11 @@ class TestDisputeAuthentication:
|
|||||||
|
|
||||||
async def test_file_dispute_with_valid_wallet_address(self, client):
|
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)"""
|
"""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(
|
response = await client.post(
|
||||||
"/rpc/disputes/file",
|
"/rpc/disputes/file",
|
||||||
json={
|
json={
|
||||||
"agreement_id": "test_agreement_1",
|
"agreement_id": 1,
|
||||||
"respondent": "0x1234567890123456789012345678901234567890",
|
"respondent": "0x1234567890123456789012345678901234567890",
|
||||||
"dispute_type": "payment_dispute",
|
"dispute_type": "payment_dispute",
|
||||||
"reason": "Test dispute",
|
"reason": "Test dispute",
|
||||||
@@ -144,12 +155,29 @@ class TestDisputeAuthentication:
|
|||||||
# May be 500 if dispute service is not available, which is acceptable
|
# May be 500 if dispute service is not available, which is acceptable
|
||||||
assert response.status_code != status.HTTP_401_UNAUTHORIZED
|
assert response.status_code != status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
async def test_jwt_token_not_implemented(self, client):
|
async def test_valid_wallet_address_header_is_not_trusted_by_default(self, client):
|
||||||
"""Test that JWT token authentication returns 501 (not yet implemented)"""
|
"""Test that filing a dispute with an untrusted wallet address header returns 401"""
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/rpc/disputes/file",
|
"/rpc/disputes/file",
|
||||||
json={
|
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",
|
"respondent": "0x1234567890123456789012345678901234567890",
|
||||||
"dispute_type": "payment_dispute",
|
"dispute_type": "payment_dispute",
|
||||||
"reason": "Test dispute",
|
"reason": "Test dispute",
|
||||||
@@ -158,18 +186,18 @@ class TestDisputeAuthentication:
|
|||||||
headers={"Authorization": "Bearer test_token"}
|
headers={"Authorization": "Bearer test_token"}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
assert "JWT authentication not yet implemented" in response.json()["detail"]
|
assert "JWT authentication is not supported" in response.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
class TestDisputeAuthDevMode:
|
class TestDisputeAuthDevMode:
|
||||||
"""Test authentication behavior in development mode"""
|
"""Test authentication behavior in development mode"""
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest_asyncio.fixture
|
||||||
async def dev_client(self):
|
async def dev_client(self):
|
||||||
"""Create test client with DEV_MODE enabled"""
|
"""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
|
from fastapi import FastAPI
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -177,7 +205,9 @@ class TestDisputeAuthDevMode:
|
|||||||
|
|
||||||
# Set DEV_MODE to true
|
# Set DEV_MODE to true
|
||||||
original_dev_mode = os.getenv("DEV_MODE")
|
original_dev_mode = os.getenv("DEV_MODE")
|
||||||
|
original_trust_header = os.getenv("TRUST_X_WALLET_ADDRESS")
|
||||||
os.environ["DEV_MODE"] = "true"
|
os.environ["DEV_MODE"] = "true"
|
||||||
|
os.environ["TRUST_X_WALLET_ADDRESS"] = "false"
|
||||||
|
|
||||||
transport = ASGITransport(app=app)
|
transport = ASGITransport(app=app)
|
||||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||||
@@ -189,12 +219,12 @@ class TestDisputeAuthDevMode:
|
|||||||
else:
|
else:
|
||||||
os.environ["DEV_MODE"] = original_dev_mode
|
os.environ["DEV_MODE"] = original_dev_mode
|
||||||
|
|
||||||
async def test_file_dispute_dev_mode_fallback(self, dev_client):
|
async def test_file_dispute_dev_mode_fails_closed(self, dev_client):
|
||||||
"""Test that in dev mode, missing auth uses zero address fallback"""
|
"""Test that dev mode no longer uses a zero address fallback"""
|
||||||
response = await dev_client.post(
|
response = await dev_client.post(
|
||||||
"/rpc/disputes/file",
|
"/rpc/disputes/file",
|
||||||
json={
|
json={
|
||||||
"agreement_id": "test_agreement_1",
|
"agreement_id": 1,
|
||||||
"respondent": "0x1234567890123456789012345678901234567890",
|
"respondent": "0x1234567890123456789012345678901234567890",
|
||||||
"dispute_type": "payment_dispute",
|
"dispute_type": "payment_dispute",
|
||||||
"reason": "Test dispute",
|
"reason": "Test dispute",
|
||||||
@@ -202,6 +232,22 @@ class TestDisputeAuthDevMode:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# In dev mode, should not return 401 (uses zero address fallback)
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
# May return 500 if dispute service is not available
|
assert "Authentication required" in response.json()["detail"]
|
||||||
assert response.status_code != status.HTTP_401_UNAUTHORIZED
|
|
||||||
|
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