Root cause
Authentication & access control introduced
security: Fix remaining CORS vulnerabilities (CWE-942)
How AI contributed: Direct introductionPublished Aug 25, 2026 in
mervinpraison/praisonaiAll 19 findings in this repository
The PraisonAI browser gateway checked Chrome extension origins with an unanchored pattern, so BrowserServer._handle_connection() in src/praisonai/praisonai/browser/server.py accepted any Origin that started with chrome-extension:// plus 32 alphanumeric characters. Because nothing else authenticated the caller before websocket.accept(), a page on any domain could open the debugging socket and have the server run a BrowserAgent goal the page supplied, exposing cookies, screenshots, and browser actions. The fix anchored the expression in both the CORS middleware and the handshake check.
BrowserServer._handle_connection() matched the Origin header with re.match(r"chrome-extension://[a-z0-9]{32}", origin), and re.match anchors only at the start of the string. A 33-character extension identifier, or an identifier followed by extra characters, therefore passed the check, and no second authentication factor was required before websocket.accept().
Root cause
security: Fix remaining CORS vulnerabilities (CWE-942)
Fix
refactor: harden input validation and query parameterization
Case facts
@@ -85,16 +85,22 @@ def _get_app(self): # Configure CORS origins based on environment cors_origins = os.getenv("BROWSER_CORS_ORIGINS", "").split(",")- cors_origins = [origin.strip() for origin in cors_origins if origin.strip()]+ cors_origins = [origin.strip() for origin in cors_origins if origin.strip() and origin.strip() != "*"] # Default secure origins if none specified if not cors_origins:- cors_origins = [- "http://localhost:3000", # Development frontend- "http://localhost:8000", # Local development- "http://127.0.0.1:3000", # Local development- "http://127.0.0.1:8000", # Local development- ]+ # Environment-specific defaults for security+ if os.getenv("ENVIRONMENT") == "production":+ # In production, require explicit configuration+ cors_origins = []+ else:+ # Development defaults - restrict to local origins+ cors_origins = [+ "http://localhost:3000", # Development frontend+ "http://localhost:8000", # Local development+ "http://127.0.0.1:3000", # Local development+ "http://127.0.0.1:8000", # Local development+ ] # Enable CORS for extension with secure origins. # allow_origin_regex enables Chrome extension support since extension IDs@@ -132,20 +138,32 @@ async def _handle_connection(self, websocket): import time import uuid import os+ import re+ + # Use same CORS origins configuration for WebSocket validation+ cors_origins = os.getenv("BROWSER_CORS_ORIGINS", "").split(",")+ cors_origins = [origin.strip() for origin in cors_origins if origin.strip() and origin.strip() != "*"]+ + if not cors_origins:+ if os.getenv("ENVIRONMENT") == "production":+ cors_origins = []+ else:+ cors_origins = [+ "http://localhost:3000", "http://localhost:8000",+ "http://127.0.0.1:3000", "http://127.0.0.1:8000"+ ] origin = websocket.headers.get("origin")- allowed_origins = os.environ.get("ALLOWED_ORIGINS", "").split(",") if origin: import urllib.parse parsed_origin = urllib.parse.urlparse(origin) is_allowed = False - if parsed_origin.scheme in ("http", "https") and parsed_origin.hostname in ("localhost", "127.0.0.1"):- is_allowed = True- elif parsed_origin.scheme == "chrome-extension":+ # Check exact origin matches+ if origin in cors_origins: is_allowed = True- - if any(origin == allowed.strip() for allowed in allowed_origins if allowed.strip()):+ # Check chrome extension regex pattern (same as CORS middleware)+ elif parsed_origin.scheme == "chrome-extension" and re.match(r"chrome-extension://[a-z0-9]{32}", origin): is_allowed = True if not is_allowed:@@ -71,16 +71,23 @@ def __init__( self.host = host self.port = port self.endpoint = endpoint- # Default to localhost origins for security+ # Environment-aware CORS origins for security if cors_origins is None:- self.cors_origins = [- "http://localhost:3000",- "http://127.0.0.1:3000", - "http://localhost:8000",- "http://127.0.0.1:8000"- ]+ import os+ if os.getenv("ENVIRONMENT") == "production":+ # In production, require explicit configuration+ self.cors_origins = []+ else:+ # Development defaults - restrict to local origins+ self.cors_origins = [+ "http://localhost:3000",+ "http://127.0.0.1:3000", + "http://localhost:8000",+ "http://127.0.0.1:8000"+ ] else:- self.cors_origins = cors_origins+ # Validate provided origins to reject wildcards+ self.cors_origins = [origin for origin in cors_origins if origin != "*"] self.api_key = api_key self.session_ttl = session_ttl self.allow_client_termination = allow_client_termination@@ -90,7 +90,7 @@ def create_app( if cors_origins is None: # Default secure origins based on environment default_origins = os.getenv("JOBS_CORS_ORIGINS", "").split(",")- default_origins = [origin.strip() for origin in default_origins if origin.strip()]+ default_origins = [origin.strip() for origin in default_origins if origin.strip() and origin.strip() != "*"] if not default_origins: # Secure defaults for different environments@@ -114,7 +114,7 @@ def create_app( allow_origins=origins, allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],- allow_headers=["Authorization", "Content-Type", "Origin", "Accept"],+ allow_headers=["Authorization", "Content-Type", "Origin", "Accept", "Idempotency-Key"], ) # Add jobs router@@ -120,7 +120,7 @@ def _get_app(self): app.add_middleware( CORSMiddleware, allow_origins=cors_origins,- allow_origin_regex=r"chrome-extension://[a-z0-9]{32}",+ allow_origin_regex=r"chrome-extension://[a-z0-9]{32}$", allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["Authorization", "Content-Type", "Origin", "Accept"],@@ -183,7 +183,7 @@ async def _handle_connection(self, websocket): if origin in cors_origins: is_allowed = True # Check chrome extension regex pattern (same as CORS middleware)- elif parsed_origin.scheme == "chrome-extension" and re.match(r"chrome-extension://[a-z0-9]{32}", origin):+ elif parsed_origin.scheme == "chrome-extension" and re.match(r"chrome-extension://[a-z0-9]{32}$", origin): is_allowed = True if not is_allowed:AI-assisted change 77fc600b48d7f40202c609d2efe6404e8018f2246b62a097e7826b50f945bb18 · Fix 632dbe1a3b4453dd41aa9e2fc3042f4277f4e95cc1ef5d0cb2bdedfeea75f9d5