[GH-ISSUE #313] bug: save_config() does not escape newlines -> invalid config.toml -> silent loss of all config sections (incl. server.secret) #227

Open
opened 2026-10-18 14:03:58 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @adonay52-svg on GitHub (Oct 9, 2026).
Original GitHub issue: https://github.com/open-webui/computer/issues/313

Summary

save_config() in cptr/utils/config.py serializes string values without escaping newlines. Any config value that contains a real newline (a multi-line system prompt, or any setting saved from a UI textarea) is written as a literal line break inside a basic TOML string, which is invalid TOML.

load_config() then swallows the resulting TOMLDecodeError and falls back to an empty cache (except Exception: _config_cache = {}). The first write that happens afterwards — sync_config_to_toml() calls save_config() after every Config.upsert() — persists that empty config. Result: every other section of config.toml is silently destroyed, including [server].secret, [auth] and all API keys / bot tokens kept in [app_config].

No exception is raised, nothing is logged, and the damage surfaces much later — the user just finds that sessions were invalidated and settings reverted.

Affected

  • cptr/utils/config.py — save_config() (lines ~116-141), load_config() (lines ~45-67)
  • Branch: main (the same pattern is present in release)
  • Version observed: 0.9.21 (pyproject.toml)
  • Last change to the file: 43380c2 ("refac", 2026-07-23)

Steps to reproduce

Stdlib only, independent of this install (tomllib needs Python >= 3.11):

import tomllib

def save_config_0921(config: dict) -> str:
    """Copy of cptr 0.9.21 cptr/utils/config.py::save_config()"""
    lines = []
    for section, values in config.items():
        if isinstance(values, dict):
            lines.append(f"[{section}]")
            for k, v in values.items():
                key_str = f'"{k}"' if "." in k else k
                if isinstance(v, str):
                    escaped = v.replace("\\", "\\\\").replace('"', '\\"')  # newline NOT escaped
                    lines.append(f'{key_str} = "{escaped}"')
                elif isinstance(v, bool):
                    lines.append(f"{key_str} = {'true' if v else 'false'}")
                elif isinstance(v, (int, float)):
                    lines.append(f"{key_str} = {v}")
                else:
                    lines.append(f"{key_str} = {v}")
            lines.append("")
    return "\n".join(lines)

config = {
    "app_config": {"subagents.system_prompt": "Line one\nLine two"},
    "server": {"secret": "deadbeef", "port": 8000},
    "auth": {"signup_enabled": False},
}

written = save_config_0921(config)
print(written)
# ... "subagents.system_prompt" = "Line one
#     Line two"        <- literal newline inside a basic string: invalid TOML

try:
    parsed = tomllib.loads(written)
except Exception as e:
    print(type(e).__name__, e)   # TOMLDecodeError  -> load_config() returns {}
    parsed = {}

print(save_config_0921(parsed))
# next save: config.toml is overwritten with an EMPTY file (nothing left to write)

In the application the trigger path is:

  1. Any multi-line value is stored in the DB (app_config) — e.g. subagents.system_prompt.

  2. sync_config_to_toml() runs after the upsert (cptr/utils/config.py ~line 152):

    config = load_config()
    ...
    config["app_config"] = toml_section
    save_config(config)          # writes the invalid file
    
  3. On restart (or any cache invalidation), load_config() hits the invalid TOML and returns {}.

  4. The next setting change calls sync_config_to_toml() again: config is {}, so the "merged" result is only {"app_config": {...}} and everything else is gone.

Expected

Multi-line values either round-trip correctly, or at minimum the file stays valid TOML and no other section is lost.

Actual

config.toml is written as invalid TOML, then silently rewritten as (almost) empty, taking all unrelated sections and secrets with it.

Impact

  • Total, silent loss of config.toml contents other than the section being saved.
  • [server].secret is regenerated on the next _get_jwt_secret() call → all existing JWT sessions are invalidated.
  • [auth] settings and every credential stored in [app_config] (provider API keys, messaging bot tokens) are lost without any error message.
  • Affects any user who ever saves a value containing a newline through the UI.

Suggested fix

Serialize string values with a real TOML writer, or at minimum JSON-encode them (json.dumps always produces a valid TOML basic string):

import json as _json

if isinstance(v, str):
    lines.append(f"{key_str} = {_json.dumps(v)}")

and decode symmetrically where strings are read back.

Independently, load_config() should stop treating "cannot parse" as "no config": catching Exception and returning {} is what turns a formatting bug into data loss. Failure to parse an existing file should be logged and surfaced, not silently degraded.

Notes

  • Searched existing issues for save_config / TOML / config loss: no match.
  • Checked both main and release: the pattern is the same in both.
  • I found and reproduced this on 0.9.21 and patched my own install locally, so the repro above is deliberately stdlib-only and does not depend on my environment.
Originally created by @adonay52-svg on GitHub (Oct 9, 2026). Original GitHub issue: https://github.com/open-webui/computer/issues/313 ### Summary `save_config()` in `cptr/utils/config.py` serializes string values without escaping newlines. Any config value that contains a real newline (a multi-line system prompt, or any setting saved from a UI textarea) is written as a literal line break inside a basic TOML string, which is invalid TOML. `load_config()` then swallows the resulting `TOMLDecodeError` and falls back to an empty cache (`except Exception: _config_cache = {}`). The first write that happens afterwards — `sync_config_to_toml()` calls `save_config()` after every `Config.upsert()` — persists that empty config. Result: every other section of `config.toml` is silently destroyed, including `[server].secret`, `[auth]` and all API keys / bot tokens kept in `[app_config]`. No exception is raised, nothing is logged, and the damage surfaces much later — the user just finds that sessions were invalidated and settings reverted. ### Affected - `cptr/utils/config.py` — `save_config()` (lines ~116-141), `load_config()` (lines ~45-67) - Branch: `main` (the same pattern is present in `release`) - Version observed: **0.9.21** (`pyproject.toml`) - Last change to the file: `43380c2` ("refac", 2026-07-23) ### Steps to reproduce Stdlib only, independent of this install (`tomllib` needs Python >= 3.11): ```python import tomllib def save_config_0921(config: dict) -> str: """Copy of cptr 0.9.21 cptr/utils/config.py::save_config()""" lines = [] for section, values in config.items(): if isinstance(values, dict): lines.append(f"[{section}]") for k, v in values.items(): key_str = f'"{k}"' if "." in k else k if isinstance(v, str): escaped = v.replace("\\", "\\\\").replace('"', '\\"') # newline NOT escaped lines.append(f'{key_str} = "{escaped}"') elif isinstance(v, bool): lines.append(f"{key_str} = {'true' if v else 'false'}") elif isinstance(v, (int, float)): lines.append(f"{key_str} = {v}") else: lines.append(f"{key_str} = {v}") lines.append("") return "\n".join(lines) config = { "app_config": {"subagents.system_prompt": "Line one\nLine two"}, "server": {"secret": "deadbeef", "port": 8000}, "auth": {"signup_enabled": False}, } written = save_config_0921(config) print(written) # ... "subagents.system_prompt" = "Line one # Line two" <- literal newline inside a basic string: invalid TOML try: parsed = tomllib.loads(written) except Exception as e: print(type(e).__name__, e) # TOMLDecodeError -> load_config() returns {} parsed = {} print(save_config_0921(parsed)) # next save: config.toml is overwritten with an EMPTY file (nothing left to write) ``` In the application the trigger path is: 1. Any multi-line value is stored in the DB (`app_config`) — e.g. `subagents.system_prompt`. 2. `sync_config_to_toml()` runs after the upsert (`cptr/utils/config.py` ~line 152): ```python config = load_config() ... config["app_config"] = toml_section save_config(config) # writes the invalid file ``` 3. On restart (or any cache invalidation), `load_config()` hits the invalid TOML and returns `{}`. 4. The next setting change calls `sync_config_to_toml()` again: `config` is `{}`, so the "merged" result is only `{"app_config": {...}}` and **everything else is gone**. ### Expected Multi-line values either round-trip correctly, or at minimum the file stays valid TOML and no other section is lost. ### Actual `config.toml` is written as invalid TOML, then silently rewritten as (almost) empty, taking all unrelated sections and secrets with it. ### Impact - Total, silent loss of `config.toml` contents other than the section being saved. - `[server].secret` is regenerated on the next `_get_jwt_secret()` call → **all existing JWT sessions are invalidated**. - `[auth]` settings and every credential stored in `[app_config]` (provider API keys, messaging bot tokens) are lost without any error message. - Affects any user who ever saves a value containing a newline through the UI. ### Suggested fix Serialize string values with a real TOML writer, or at minimum JSON-encode them (`json.dumps` always produces a valid TOML basic string): ```python import json as _json if isinstance(v, str): lines.append(f"{key_str} = {_json.dumps(v)}") ``` and decode symmetrically where strings are read back. Independently, `load_config()` should stop treating "cannot parse" as "no config": catching `Exception` and returning `{}` is what turns a formatting bug into data loss. Failure to parse an *existing* file should be logged and surfaced, not silently degraded. ### Notes - Searched existing issues for `save_config` / TOML / config loss: no match. - Checked both `main` and `release`: the pattern is the same in both. - I found and reproduced this on 0.9.21 and patched my own install locally, so the repro above is deliberately stdlib-only and does not depend on my environment.
Author
Owner

@owui-terminator[bot] commented on GitHub (Oct 9, 2026):

⚠️ Missing Issue Title Prefix

@adonay52-svg, your issue title is missing a categorising prefix (e.g. bug:, feat:, docs:).

Please update the title to lead with one of:

  • bug: bug report or error
  • feat: feature request or enhancement
  • docs: documentation issue
  • question: usage question
  • help: support request

Example: bug: Login fails when the password contains special characters

<!-- gh-comment-id:6072387527 --> @owui-terminator[bot] commented on GitHub (Oct 9, 2026): # ⚠️ Missing Issue Title Prefix @adonay52-svg, your issue title is missing a categorising prefix (e.g. `bug:`, `feat:`, `docs:`). Please update the title to lead with one of: - **bug**: bug report or error - **feat**: feature request or enhancement - **docs**: documentation issue - **question**: usage question - **help**: support request Example: `bug: Login fails when the password contains special characters`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-webui/computer#227
No description provided.