Research
case-studysecurity7 min read

Tabby PR #11228 requires HTTPS for config sync after YAML profiles reached command execution

PR #11228 in Eugeny/tabby blocks cleartext config sync because a tampered YAML response could inject terminal profiles that later execute commands.

Tabby PR #11228 requires HTTPS for config sync after YAML profiles reached command execution

Eugeny/tabby accepted cleartext http:// hosts for configuration sync until PR #11228 changed the sync chokepoint to require HTTPS. The bug was not that Tabby fetched settings from a remote server. The bug was that those settings were YAML, were merged into the live application configuration and could define terminal profiles whose command and env fields are later used by the terminal layer.

That makes the sync payload executable-equivalent. If an on-path attacker can tamper with it, the next synchronisation can become a command injection path under the user's account.

The vulnerable path

The relevant code lives in tabby-settings/src/services/configSync.service.ts. Around line 163, ConfigSyncService.request() normalised the configured host by trimming a trailing slash, then concatenated it directly with the API path:

if (this.config.store.configSync.host.endsWith('/')) {
    this.config.store.configSync.host = this.config.store.configSync.host.slice(0, -1)
}
url = this.config.store.configSync.host + url

The service then called axios.request(...) with the resulting URL. Axios does not silently upgrade http:// to https://. It honours the scheme it is given. If the user configured a LAN sync endpoint, a self-hosted service or any other URL beginning with http://, Tabby would fetch the configuration over a plaintext channel.

The data flow is what made this more than a transport hardening issue:

  1. The user configures configSync.host with an http:// URL.
  2. ConfigSyncService.request() concatenates that host with the API path and sends the request through axios.
  3. The sync response is parsed as YAML.
  4. The parsed YAML is merged into config.store.
  5. Profile entries from that configuration can include command and env values that are later consumed when the terminal opens a profile.

An attacker with a realistic on-path position, such as a rogue Wi-Fi access point, ARP spoofing position on a LAN, DNS manipulation path or hostile network provider, can tamper with the cleartext response. The injected profile does not need a memory corruption bug or a deserialisation gadget. It only needs to place attacker-controlled command configuration where Tabby already expects command configuration to be.

Why the impact is command execution

Configuration is often treated as lower risk than code because it is not usually executed directly. Terminal profiles are different. A profile is an instruction set for launching a process. If a synced profile controls the executable, arguments or environment, it sits on the boundary between settings and execution.

The exploit chain is therefore concrete:

  1. Intercept a cleartext config sync response.
  2. Replace or extend the YAML with a malicious profile.
  3. Let Tabby merge the YAML into local configuration.
  4. Wait for the user to start the profile or for any code path that consumes the profile.
  5. The configured command runs under the user's account.

That is local command execution reached through a network tampering primitive. The attacker does not already have code execution by being on the network. They have the ability to observe or modify traffic that lacks integrity. The vulnerable sync path converts that network position into a local execution path.

The most accurate primary CWE mapping is CWE-319: cleartext transmission of sensitive information. The security-sensitive material here is not only confidential configuration. It is configuration that influences process creation. CWE-295 is adjacent because the missing property is authenticated transport, but with http:// there is no certificate validation step to perform at all.

The fix in PR #11228

The fix changes the same chokepoint before the axios call. Instead of concatenating the configured host immediately, request() stores it in host, validates the scheme and throws before any network request if the host is not HTTPS:

const host: string = this.config.store.configSync.host
if (!/^https:\/\//i.test(host)) {
    const message = `Config sync host must use HTTPS (got: ${host})`
    this.logger.error(message)
    throw new Error(message)
}
url = host + url

The regular expression is deliberately narrow. It accepts https:// in any case. It rejects http://, ftp://, protocol-relative URLs such as //example.com, scheme-confusion strings such as https-evil://x, leading-whitespace tricks and an empty host.

That matters because URL validation bugs often come from accepting strings that merely contain a safe-looking token. This check anchors the scheme at the start of the string and requires the literal :// separator. It is not a full URL parser, but it does not need to be. The security decision here is binary: this service must not use a non-HTTPS transport.

The placement is also important. request() is the shared path for getConfigs, getConfig, setConfig and deleteConfig. There was no parallel HTTP entrypoint in the service, so guarding request() blocks the cleartext path for all config sync operations without duplicating validation across call sites.

The trade-off is intentional. Users who configured a plaintext LAN endpoint now receive an explicit error. That is the right failure mode. A local sync server without TLS may feel convenient, but the payload being fetched is not harmless preferences. It can shape what Tabby later executes.

The broader pattern: configuration becoming code

Tabby is not an AI application, but the bug sits in the same pattern that has been showing up across AI and agent tooling: configuration files are increasingly treated as behaviour.

In the post on LangFlow, n8n and configuration as code execution, the recurring failure was direct. Platforms accepted workflow configuration, prompt definitions or node settings and evaluated them in privileged contexts. CVE-2025-3248 in LangFlow is the bluntest version of that class: a validation endpoint that resulted in pre-authentication remote code execution and was later added to CISA's Known Exploited Vulnerabilities catalogue. CVE-2024-46946 in LangChain reached code execution through prompt loading and unsafe object construction.

Tabby's case is quieter. There is no exec() call in the diff. The danger comes from the semantics of the data after it is merged. A YAML document that defines terminal profiles is not equivalent to a theme file or a window layout. It is closer to a launch script.

The same distinction appears in AI agent ecosystems. MCP server definitions, tool descriptions, skills and workspace rules all look like text configuration until a model or runtime acts on them. In the post on MCP's attack surface, the core issue was that tool metadata could influence execution. In the post on prompt injection in AI coding assistants, ordinary project files became instruction sources once an assistant consumed them.

The lesson transfers cleanly: the risk of a configuration format is determined by the most privileged action it can trigger, not by the file extension or the parser. YAML that can become a spawned command deserves transport integrity. So does JSON that configures an agent tool. So does Markdown that an assistant treats as operating instructions.

Why HTTPS is the right boundary

Requiring HTTPS does not make a malicious sync server safe. If the user chooses to trust an attacker-controlled endpoint, authenticated transport will faithfully deliver hostile configuration. That is a different trust problem.

This fix addresses the network attacker case. With http://, a user can choose a legitimate sync server and still receive attacker-modified YAML because the channel provides no integrity or authenticity. With https://, axios on Node uses certificate validation by default. A passive or active network attacker can still block the connection, but they cannot silently impersonate the sync host without defeating TLS.

That is the boundary this service needed. It does not try to sanitise every possible profile value, because Tabby profiles are meant to launch commands. It prevents an unauthorised network party from rewriting those values in transit.

Small transport bugs are easy to underestimate when the patch is only a dozen lines. This one was not small because the transport carried executable-equivalent configuration. The right question for any sync feature is not just whether the data is sensitive. It is what the application will do after it believes the data.

Newsletter

One email a week. Security research, engineering deep-dives and AI security insights - written for practitioners. No noise.