T09 · Insecure Skill Coding Practices
- Location
oauth_server.py:63- Finding
Unauthenticated OAuth onboarding permits path traversal and client configuration overwrite
- Content
View full analysis
Vulnerability Details
File Location:
oauth_server.py:63-69, 86-90, 106-120
Vulnerability Type: Unauthenticated path traversal and arbitrary configuration overwrite
Risk Level: HighVulnerable Code:
python @app.route("/auth/start") def auth_start(): """Client visits this URL to begin authorization.""" client_name = request.args.get("client", "").strip() if not client_name: return ( "<h2>Missing client name</h2>" "<p>Add <code>?client=business-name</code> to the URL.</p>" "<p>Example: <code>/auth/start?client=joes-pizza</code></p>" ), 400 session["client_name"] = client_namepython @app.route(REDIRECT_PATH) def auth_callback(): """Google redirects here after the client authorizes.""" client_name = session.get("client_name", "unknown") flow = get_flow() flow.fetch_token(authorization_response=request.url) creds = flow.credentials refresh_token = creds.refresh_tokenpython # Save the client config CLIENTS_DIR.mkdir(parents=True, exist_ok=True) config_path = CLIENTS_DIR / f"{client_name}.json" config = { "business_name": client_name, "account_id": "NEEDS_LOOKUP", "location_id": "NEEDS_LOOKUP", "oauth_client_id": GOOGLE_CLIENT_ID, "oauth_client_secret": GOOGLE_CLIENT_SECRET, "refresh_token": refresh_token, "authorized_at": datetime.now(timezone.utc).isoformat(), "notes": "", } with open(config_path, "w") as f: json.dump(config, f, indent=2)Technical Analysis
The public
/auth/startendpoint accepts theclientquery parameter without authentication or syntactic validation. This value is stored in the Flask session and later used directly to construct a filesystem path:python config_path = CLIENTS_DIR / f"{client_name}.json"pathlibdoes not confine the resulting path toCLIENTS_DIR. A value containing traversa ...[truncated 2312 chars]- Remediation
View remediation
Remediation Suggestions
-
Restrict client identifiers to a strict allowlist:
python import re if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", client_name): return "Invalid client identifier", 400 -
Resolve the destination path and verify containment before writing:
python clients_root = CLIENTS_DIR.resolve() config_path = (clients_root / f"{client_name}.json").resolve() if config_path.parent != clients_root: return "Invalid destination", 400 -
Require an authenticated operator to generate a random, single-use onboarding invitation bound to one predetermined client ID. Do not let the onboarding participant choose the destination identifier.
-
Reject attempts to replace an existing configuration unless an authenticated operator explicitly initiates a rotation workflow.
-
Create new files atomically with exclusive semantics, such as mode
"x", and set restrictive permissions because the files contain OAuth secrets. -
Deploy the onboarding endpoint behind HTTPS and appropriate access controls rather than exposing the Flask development server directly.
-
