Skip to content

Commit dd41a3c

Browse files
praisonai-triage-agent[bot]CopilotMervinPraison
authored
fix(cors): Patch 3 follow-up bugs from CWE-942 CORS hardening (#1323)
* security: Fix CORS Configuration Vulnerabilities (CWE-942) - Replace wildcard origins - Replace allow_origins=['*'] with secure environment-based configurations - Update browser server to use BROWSER_CORS_ORIGINS env var with localhost defaults - Update jobs server to use JOBS_CORS_ORIGINS env var with localhost defaults - Update example API to use API_CORS_ORIGINS env var with localhost defaults - Remove wildcard defaults from ServerConfig, GatewayConfig, and AppConfig - Add secure localhost defaults to MCP server transport - Update tests and documentation to reflect secure defaults - Restrict CORS methods and headers to essential ones only Fixes #1321 - Addresses 3 instances of CWE-942 vulnerabilities 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com> * fix: Address 3 CORS bugs found in review - chrome-extension regex, http_stream headers, server.py methods/headers Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/b76a2075-da18-4ad5-82e8-79fc09457529 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com> * security: Fix remaining CORS vulnerabilities (CWE-942) - Add environment-based CORS defaults (empty for production) - Validate env vars to reject wildcard origins (*) - Add missing Idempotency-Key header to jobs CORS - Unify WebSocket and HTTP CORS origin validation - Make http_stream transport environment-aware Fixes all critical security issues identified by code reviewers: - Production no longer allows localhost by default - Environment variables cannot bypass security with * - WebSocket uses same origin validation as CORS middleware - Jobs API properly supports Idempotency-Key header 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com> --------- Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com> Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
1 parent e87cc65 commit dd41a3c

File tree

10 files changed

+133
-39
lines changed

10 files changed

+133
-39
lines changed

examples/python/api/secondary-market-research-api.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,34 @@
4343
redoc_url="/redoc"
4444
)
4545

46-
# Add CORS middleware
47-
app.add_middleware(
48-
CORSMiddleware,
49-
allow_origins=["*"],
50-
allow_credentials=True,
51-
allow_methods=["*"],
52-
allow_headers=["*"],
53-
)
46+
# Add CORS middleware with secure configuration
47+
cors_origins = os.getenv("API_CORS_ORIGINS", "").split(",")
48+
cors_origins = [origin.strip() for origin in cors_origins if origin.strip() and origin.strip() != "*"]
49+
50+
# Default secure origins if none specified
51+
if not cors_origins:
52+
# Secure defaults for different environments
53+
if os.getenv("ENVIRONMENT") == "production":
54+
# In production, require explicit configuration
55+
cors_origins = []
56+
else:
57+
# Development defaults - restrict to local origins
58+
cors_origins = [
59+
"http://localhost:3000", # Development frontend
60+
"http://localhost:8000", # Local development
61+
"http://127.0.0.1:3000", # Local development
62+
"http://127.0.0.1:8000", # Local development
63+
]
64+
65+
# Only add CORS middleware if origins are specified
66+
if cors_origins:
67+
app.add_middleware(
68+
CORSMiddleware,
69+
allow_origins=cors_origins,
70+
allow_credentials=True,
71+
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
72+
allow_headers=["Authorization", "Content-Type", "Origin", "Accept"],
73+
)
5474

5575
# Create directories for storing reports
5676
REPORTS_DIR = Path("generated_reports")

src/praisonai-agents/praisonaiagents/app/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class AgentAppConfig:
2121
host: Host address to bind to (default: "0.0.0.0")
2222
port: Port number to listen on (default: 8000)
2323
reload: Enable auto-reload for development (default: False)
24-
cors_origins: List of allowed CORS origins (default: ["*"])
24+
cors_origins: List of allowed CORS origins (default: [])
2525
api_prefix: API route prefix (default: "/api")
2626
docs_url: URL for API documentation (default: "/docs")
2727
openapi_url: URL for OpenAPI schema (default: "/openapi.json")
@@ -44,7 +44,7 @@ class AgentAppConfig:
4444
host: str = "0.0.0.0"
4545
port: int = 8000
4646
reload: bool = False
47-
cors_origins: List[str] = field(default_factory=lambda: ["*"])
47+
cors_origins: List[str] = field(default_factory=lambda: [])
4848
api_prefix: str = "/api"
4949
docs_url: str = "/docs"
5050
openapi_url: str = "/openapi.json"

src/praisonai-agents/praisonaiagents/gateway/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class GatewayConfig:
5757

5858
host: str = "127.0.0.1"
5959
port: int = 8765
60-
cors_origins: List[str] = field(default_factory=lambda: ["*"])
60+
cors_origins: List[str] = field(default_factory=lambda: [])
6161
auth_token: Optional[str] = None
6262
max_connections: int = 1000
6363
max_sessions_per_agent: int = 0 # 0 = unlimited
@@ -204,7 +204,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "MultiChannelGatewayConfig":
204204
gateway_config = GatewayConfig(
205205
host=gw_data.get("host", "127.0.0.1"),
206206
port=gw_data.get("port", 8765),
207-
cors_origins=gw_data.get("cors_origins", ["*"]),
207+
cors_origins=gw_data.get("cors_origins", []),
208208
auth_token=gw_data.get("auth_token"),
209209
max_connections=gw_data.get("max_connections", 1000),
210210
)

src/praisonai-agents/praisonaiagents/server/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class ServerConfig:
2727

2828
host: str = DEFAULT_HOST
2929
port: int = DEFAULT_PORT
30-
cors_origins: List[str] = field(default_factory=lambda: ["*"])
30+
cors_origins: List[str] = field(default_factory=lambda: [])
3131
auth_token: Optional[str] = None
3232
max_connections: int = 100
3333

@@ -200,8 +200,8 @@ async def info(request):
200200
app = CORSMiddleware(
201201
app,
202202
allow_origins=self.config.cors_origins,
203-
allow_methods=["*"],
204-
allow_headers=["*"],
203+
allow_methods=["GET", "POST", "OPTIONS"],
204+
allow_headers=["Authorization", "Content-Type", "Origin", "Accept"],
205205
)
206206

207207
return app

src/praisonai-agents/tests/unit/server/test_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_config_defaults(self):
2222

2323
assert config.host == "127.0.0.1"
2424
assert config.port == 8765
25-
assert config.cors_origins == ["*"]
25+
assert config.cors_origins == []
2626
assert config.auth_token is None
2727

2828
def test_config_custom(self):

src/praisonai-agents/tests/unit/test_gateway_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def test_gateway_config_defaults(self):
5353
config = GatewayConfig()
5454
assert config.host == "127.0.0.1"
5555
assert config.port == 8765
56-
assert config.cors_origins == ["*"]
56+
assert config.cors_origins == []
5757
assert config.auth_token is None
5858
assert config.max_connections == 1000
5959
assert config.max_sessions_per_agent == 0

src/praisonai/praisonai/browser/server.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88
import signal
99
import sys
10+
import os
1011
from typing import Dict, Optional, Set
1112
from dataclasses import dataclass
1213

@@ -82,13 +83,37 @@ def _get_app(self):
8283
version="1.0.0",
8384
)
8485

85-
# Enable CORS for extension
86+
# Configure CORS origins based on environment
87+
cors_origins = os.getenv("BROWSER_CORS_ORIGINS", "").split(",")
88+
cors_origins = [origin.strip() for origin in cors_origins if origin.strip() and origin.strip() != "*"]
89+
90+
# Default secure origins if none specified
91+
if not cors_origins:
92+
# Environment-specific defaults for security
93+
if os.getenv("ENVIRONMENT") == "production":
94+
# In production, require explicit configuration
95+
cors_origins = []
96+
else:
97+
# Development defaults - restrict to local origins
98+
cors_origins = [
99+
"http://localhost:3000", # Development frontend
100+
"http://localhost:8000", # Local development
101+
"http://127.0.0.1:3000", # Local development
102+
"http://127.0.0.1:8000", # Local development
103+
]
104+
105+
# Enable CORS for extension with secure origins.
106+
# allow_origin_regex enables Chrome extension support since extension IDs
107+
# (chrome-extension://<32-char-id>) cannot be listed as exact strings
108+
# and the glob pattern chrome-extension://* is NOT supported by CORSMiddleware.
109+
# Set BROWSER_CORS_ORIGINS to restrict to a specific extension ID.
86110
app.add_middleware(
87111
CORSMiddleware,
88-
allow_origins=["*"],
112+
allow_origins=cors_origins,
113+
allow_origin_regex=r"chrome-extension://[a-z0-9]{32}",
89114
allow_credentials=True,
90-
allow_methods=["*"],
91-
allow_headers=["*"],
115+
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
116+
allow_headers=["Authorization", "Content-Type", "Origin", "Accept"],
92117
)
93118

94119
@app.get("/health")
@@ -113,20 +138,32 @@ async def _handle_connection(self, websocket):
113138
import time
114139
import uuid
115140
import os
141+
import re
142+
143+
# Use same CORS origins configuration for WebSocket validation
144+
cors_origins = os.getenv("BROWSER_CORS_ORIGINS", "").split(",")
145+
cors_origins = [origin.strip() for origin in cors_origins if origin.strip() and origin.strip() != "*"]
146+
147+
if not cors_origins:
148+
if os.getenv("ENVIRONMENT") == "production":
149+
cors_origins = []
150+
else:
151+
cors_origins = [
152+
"http://localhost:3000", "http://localhost:8000",
153+
"http://127.0.0.1:3000", "http://127.0.0.1:8000"
154+
]
116155

117156
origin = websocket.headers.get("origin")
118-
allowed_origins = os.environ.get("ALLOWED_ORIGINS", "").split(",")
119157
if origin:
120158
import urllib.parse
121159
parsed_origin = urllib.parse.urlparse(origin)
122160
is_allowed = False
123161

124-
if parsed_origin.scheme in ("http", "https") and parsed_origin.hostname in ("localhost", "127.0.0.1"):
125-
is_allowed = True
126-
elif parsed_origin.scheme == "chrome-extension":
162+
# Check exact origin matches
163+
if origin in cors_origins:
127164
is_allowed = True
128-
129-
if any(origin == allowed.strip() for allowed in allowed_origins if allowed.strip()):
165+
# Check chrome extension regex pattern (same as CORS middleware)
166+
elif parsed_origin.scheme == "chrome-extension" and re.match(r"chrome-extension://[a-z0-9]{32}", origin):
130167
is_allowed = True
131168

132169
if not is_allowed:

src/praisonai/praisonai/jobs/server.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,36 @@ def create_app(
8686
lifespan=lifespan
8787
)
8888

89-
# Add CORS middleware
90-
origins = cors_origins or ["*"]
91-
app.add_middleware(
92-
CORSMiddleware,
93-
allow_origins=origins,
94-
allow_credentials=True,
95-
allow_methods=["*"],
96-
allow_headers=["*"],
97-
)
89+
# Add CORS middleware with secure defaults
90+
if cors_origins is None:
91+
# Default secure origins based on environment
92+
default_origins = os.getenv("JOBS_CORS_ORIGINS", "").split(",")
93+
default_origins = [origin.strip() for origin in default_origins if origin.strip() and origin.strip() != "*"]
94+
95+
if not default_origins:
96+
# Secure defaults for different environments
97+
if os.getenv("ENVIRONMENT") == "production":
98+
origins = [] # No origins allowed in production without explicit config
99+
else:
100+
origins = [
101+
"http://localhost:3000", # Development frontend
102+
"http://localhost:8000", # Local development
103+
"http://127.0.0.1:3000", # Local development
104+
"http://127.0.0.1:8000", # Local development
105+
]
106+
else:
107+
origins = default_origins
108+
else:
109+
origins = cors_origins
110+
111+
if origins: # Only add CORS middleware if origins are specified
112+
app.add_middleware(
113+
CORSMiddleware,
114+
allow_origins=origins,
115+
allow_credentials=True,
116+
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
117+
allow_headers=["Authorization", "Content-Type", "Origin", "Accept", "Idempotency-Key"],
118+
)
98119

99120
# Add jobs router
100121
jobs_router = create_router(get_store(), get_executor())

src/praisonai/praisonai/mcp_server/transports/http_stream.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,23 @@ def __init__(
7171
self.host = host
7272
self.port = port
7373
self.endpoint = endpoint
74-
self.cors_origins = cors_origins or ["*"]
74+
# Environment-aware CORS origins for security
75+
if cors_origins is None:
76+
import os
77+
if os.getenv("ENVIRONMENT") == "production":
78+
# In production, require explicit configuration
79+
self.cors_origins = []
80+
else:
81+
# Development defaults - restrict to local origins
82+
self.cors_origins = [
83+
"http://localhost:3000",
84+
"http://127.0.0.1:3000",
85+
"http://localhost:8000",
86+
"http://127.0.0.1:8000"
87+
]
88+
else:
89+
# Validate provided origins to reject wildcards
90+
self.cors_origins = [origin for origin in cors_origins if origin != "*"]
7591
self.api_key = api_key
7692
self.session_ttl = session_ttl
7793
self.allow_client_termination = allow_client_termination
@@ -352,7 +368,7 @@ async def root(request: Request) -> Response:
352368
CORSMiddleware,
353369
allow_origins=self.cors_origins,
354370
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
355-
allow_headers=["*"],
371+
allow_headers=["Authorization", "Content-Type", "Origin", "Accept", "Mcp-Session-Id", "Last-Event-Id"],
356372
),
357373
]
358374

src/praisonai/praisonai/recipe/serve.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
- my-recipe
3232
- another-recipe
3333
preload: true
34-
cors_origins: "*"
34+
cors_origins: "http://localhost:3000,http://localhost:8000"
3535
rate_limit: 100 # requests per minute (0 = disabled)
3636
max_request_size: 10485760 # 10MB default
3737
enable_metrics: false # Enable /metrics endpoint

0 commit comments

Comments
 (0)