mirror of
https://github.com/open-webui/open-terminal.git
synced 2026-11-05 01:27:28 +00:00
[GH-ISSUE #177] Critical Bug: Unbounded tool responses can exceed LLM request limits and permanently poison chats #104
Labels
No labels
bug
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
open-webui/open-terminal#104
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Classic298 on GitHub (Sep 17, 2026).
Original GitHub issue: https://github.com/open-webui/open-terminal/issues/177
Version
v0.13.02085cd819edcac52873dcfe1fc3e722cadc14162Description
Several model-facing Open Terminal endpoints can return arbitrarily large, or very large, response bodies. Result-count limits such as
limit,max_results, andtaildo not reliably limit serialized response bytes because one result, one line, or one process-output entry can itself be very large.When Open Terminal is connected to an LLM client, the client normally serializes the response as a tool result and appends it to the next model request. One oversized response can therefore:
This happened in production with
match_files. It is not only a theoretical resource-usage concern: one normal search call made the affected conversation unusable until its database row was repaired.Production incident
match_filessearched minified, single-line JSON files. Only nine results were returned, but two matching lines were approximately 15.7 MB and 17.6 MB.Observed sizes:
match_filesJSON response/tool result: 36,734,472 bytes;function_call_outputafter nested JSON escaping: approximately 43.7 MB;The provider rejected the continuation with:
Because the full result had already become part of chat history, reloading or retrying did not recover the conversation.
Root cause in
match_filesThe returned match contains the entire physical line
content_match()returnstextwithout any length bound:A minified JSON document is commonly one physical line, so a single match can return the complete file.
The ripgrep path bypasses the Python fallback's 1 MiB file-size check
The ripgrep path invokes
rg --jsonwithout a maximum file-size or maximum returned-line-size control (main.pylines 1183-1226). It then forwardsdata["lines"]["text"]tocontent_match().The
MAX_CONTENT_SEARCH_FILE_SIZE = 1 MiBcheck exists only in the Python fallback (main.pylines 1228-1250). Therefore behavior depends on whetherrgis installed:rg: files larger than 1 MiB are skipped;rg: files larger than 1 MiB are searched, and complete matching lines are returned.Pagination limits result count, not response bytes
limitis capped at 100, and matches per file are capped at 3, but each match'stextis unbounded. The final slice therefore does not bound the serialized payload (main.pylines 1279-1304).Even the Python fallback can theoretically return close to 100 MiB of line text: the page can contain up to 100 matching files, each just under the 1 MiB search limit, before JSON escaping overhead.
Minimal reproduction
Run Open Terminal in an environment where
rgis installed, then create a file containing one 2 MiB line:Call
match_files:The response is approximately 2 MB even though it contains one result. Increasing the one-line file to 35 MB creates a response of roughly the same size as the incident.
Audit of the other model-facing tools
I audited all operations currently included in the OpenAPI schema at the commit above. The same class of bug exists in several other endpoints.
read_filegrep_searchmax_resultslimits matches to 500 but every match contains the complete matching line with no byte/column cap. One minified JSON line can create a multi-megabyte response; 500 large lines can be much worse. It also searches files without a file-size cap.match_filesrgimplementation bypasses the fallback's 1 MiB file cap. Count pagination does not cap bytes.run_commandwaitis used and the command completes, all retained process-output entries are returned unless the caller explicitly suppliestail. The process log defaults to a 50 MB cap, which is larger than common model-provider request limits and is not an API response-size cap.get_process_statusoffset=0andtail=None, so it can return the entire retained process log.taillimits entry count rather than serialized response bytes; the normal runners read bounded chunks, but there is still no explicit API byte invariant.list_fileslist_processesget_infoOPEN_TERMINAL_INFOverbatim with no limit. This is configuration-controlled rather than filesystem/command-controlled, but can still become an oversized tool result.search_filesglob_searchread_user_terminaldisplay_filewrite_filereplace_file_contentsend_process_input{"status":"ok"}.kill_processsend_user_terminal_inputSupporting source references
read_fileawait fs.read_text(target)and then split into lines:main.pylines 702-705.main.pylines 712-727.main.pylines 729-731.main.pylines 739-745.grep_searchEach result stores
line.rstrip("\n\r")in full (main.pylines 1024-1047).max_resultsis capped by count only (main.pylines 995-997).A local source-level reproduction confirmed that a single 2,000,000-character line produces a serialized
grep_searchresult of approximately 2,000,229 bytes.run_commandandget_process_statusread_log()with optionaltail; the default isNone:main.pylines 1691-1749 andmain.pylines 1763-1808.read_log()collects every matching output record whentail=None; when supplied,tailbounds only thedequeentry count, not bytes:utils/log.pylines 190-225.env.pylines 92-99.A local test against
read_log()confirmed thattail=Nonewith twenty 1,000,000-character records serializes approximately 20,000,640 bytes. A synthetic large single record also confirms thattailis an entry-count control rather than a byte-budget control. The normal process runners usually write smaller chunks (for example, 4 KiB PTY reads), so the primary practical risk is the default unbounded number of retained entries.list_filesfs.listdir(target)collects everyos.listdir()entry and the endpoint returns the complete list (main.pylines 652-663,utils/fs.pylines 202-223).Related non-OpenAPI/UI endpoints
The primary issue is the model-facing OpenAPI tool surface above. There are also other response/resource paths worth applying the same safety principles to:
read_skillreturns the completeSKILL.mdbody with no content-size cap, although resource names are bounded to 50 entries.execute_notebook_cellserializes complete Jupyter cell outputs, including stdout, rich display data, images, and tracebacks, without a response-byte cap. These notebook routes currently useinclude_in_schema=False, but the risk applies to clients that call them directly or adapters that expose them./files/comparecan return a large, structurally amplified diff. Inputs allow up to two million extracted characters / 50,000 lines per file, and line content can appear in bothcontentandsegments./proxy/{port}/{path}buffers and returns the complete upstream body./files/viewand/files/archivereturn complete files/archives and intentionally serve UI/download use cases; they should preferably stream rather than buffer entire payloads in memory./files/uploadreads the complete upload into memory before writing it.These endpoints do not all need LLM-style truncation, but they should have explicit streaming, request, response, or memory limits appropriate to their intended use.
Expected behavior
No model-facing Open Terminal tool should be able to return a response large enough to exceed typical LLM provider limits or poison a client's persistent conversation merely because a file has a long line, a directory has many entries, or a command prints a lot of output.
Count limits should not be presented as sufficient response limits when an individual item has unbounded size.
Suggested fix
1. Add a shared model-tool output byte budget
Add a configurable server-side limit, measured after UTF-8 JSON serialization, for example:
Every endpoint included as an LLM/OpenAPI/MCP tool should either:
Example overflow metadata:
The check should account for UTF-8 and JSON escaping. Truncating a Python string to N characters is not enough.
2. Fix each high-risk endpoint semantically
match_files/grep_search: return a bounded excerpt around each match instead of the full physical line; include original line length and anexcerpt_truncatedflag. Enforce the same file-size behavior with and withoutrg. Stop accumulating results once the global byte budget is exhausted.read_file: addmax_bytes/chunked reads and continuation metadata. Default to a safe maximum even when no line range is supplied. Do not read the complete file merely to serve a small range. For binary content, return a file reference/metadata to model clients rather than an unbounded raw response or data URL.run_command/get_process_status: impose a maximum response byte count independent of log-retention size. Return the newest or next chunk with a cursor. Keeptail, but make the final selection byte-bounded too.list_files: add pagination/cursor and maximum entries, then apply the global byte cap.list_processes: cap/paginate results and consider a per-session tracked-process limit.get_info: validate/cap configured content or exclude it from the shared limit only with an explicit administrator opt-in.3. Add observability
Log and/or emit a metric whenever output is truncated, including:
Do not log the oversized content itself.
@dirkenglund commented on GitHub (Sep 29, 2026):
Same class, second endpoint and a larger measurement, from a production Open WebUI + PostgreSQL deployment (Open Terminal 0.11.34; the code path is unchanged in 0.14.0):
grep_search: each match returns the whole line,"content": line.rstrip("\n\r")(open_terminal/main.py,_search_file). A call withmax_results=30over a workspace holding a few single-line files returned ~269 MB in one tool result.Suggested fix, matching the byte-budget idea above: cap each match's
contentto a window around the match (for example 400 characters, with atruncatedflag and the full line length), and cap the whole response by bytes, not only by result count. Clients can then ask for a specific file range if they need more.Implementation by Claude (Anthropic) in collaboration with @dirkenglund.