VYPR
Moderate severityOSV Advisory· Published Jan 12, 2026· Updated Jan 12, 2026

wlc may leak API keys due to an insecure API key configuration

CVE-2026-22251

Description

wlc is a Weblate command-line client using Weblate's REST API. Prior to 1.17.0, wlc supported providing unscoped API keys in the setting. This practice was discouraged for years, but the code was never removed. This might cause the API key to be leaked to different servers.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
wlcPyPI
< 1.17.01.17.0

Affected products

1

Patches

1
aafdb507a9e6

fix(config): avoid loading key from system-wide settings

https://github.com/WeblateOrg/wlcMichal ČihařJan 7, 2026via ghsa
5 files changed · +35 34
  • wlc/config.py+20 23 modified
    @@ -8,32 +8,30 @@
     
     import os.path
     from configparser import NoOptionError, RawConfigParser
    -from typing import TYPE_CHECKING
    +from typing import cast
     
    -from xdg.BaseDirectory import load_config_paths  # type: ignore[import-untyped]
    +from xdg.BaseDirectory import load_first_config  # type: ignore[import-untyped]
     
     import wlc
     
    -if TYPE_CHECKING:
    -    from collections.abc import Generator
    -
     __all__ = ["NoOptionError", "WeblateConfig"]
     
     
     class WeblateConfig(RawConfigParser):
         """Configuration parser wrapper with defaults."""
     
    -    def __init__(self, section="weblate") -> None:
    +    def __init__(self, section: str = "weblate") -> None:
             """Construct WeblateConfig object."""
             super().__init__(delimiters=("=",))
    -        self.section = section
    +        self.section: str = section
    +        self.cli_key: str | None = None
    +        self.cli_url: str | None = None
             self.set_defaults()
     
         def set_defaults(self) -> None:
             """Set default values."""
             self.add_section("keys")
             self.add_section(self.section)
    -        self.set(self.section, "key", "")
             self.set(self.section, "url", wlc.API_URL)
             self.set(self.section, "retries", "0")
             self.set(self.section, "timeout", "300")
    @@ -44,23 +42,27 @@ def set_defaults(self) -> None:
             self.set(self.section, "backoff_factor", "0")
     
         @staticmethod
    -    def find_configs() -> Generator[str]:
    +    def find_config() -> str | None:
             # Handle Windows specifically
             for envname in ("APPDATA", "LOCALAPPDATA"):
                 if path := os.environ.get(envname):
                     win_path = os.path.join(path, "weblate.ini")
                     if os.path.exists(win_path):
    -                    yield win_path
    +                    return win_path
     
             # Generic XDG paths
    -        yield from load_config_paths("weblate")
    -        yield from load_config_paths("weblate.ini")
    +        for filename in ("weblate", "weblate.ini"):
    +            if config := load_first_config(filename):
    +                return config
    +
    +        return None
     
    -    def load(self, path=None) -> None:
    +    def load(self, path: str | None = None) -> None:
             """Load configuration from XDG paths."""
             if path is None:
    -            path = list(self.find_configs())
    -        self.read(path)
    +            path = self.find_config()
    +        if path:
    +            self.read(path)
     
             # Try reading from current dir
             cwd = os.path.abspath(".")
    @@ -74,15 +76,10 @@ def load(self, path=None) -> None:
                 prev = cwd
                 cwd = os.path.dirname(cwd)
     
    -    def get_url_key(self):
    +    def get_url_key(self) -> tuple[str, str]:
             """Get API URL and key."""
    -        url = self.get(self.section, "url")
    -        key = self.get(self.section, "key")
    -        if not key:
    -            try:
    -                key = self.get("keys", url)
    -            except NoOptionError:
    -                key = ""
    +        url = self.cli_url or cast("str", self.get(self.section, "url"))
    +        key = self.cli_key or cast("str", self.get("keys", url, fallback=""))
             return url, key
     
         def get_request_options(self):
    
  • wlc/main.py+4 4 modified
    @@ -794,10 +794,10 @@ def parse_settings(args, settings):
             for section, key, value in settings:
                 config.set(section, key, value)
     
    -    for override in ("key", "url"):
    -        value = getattr(args, override)
    -        if value is not None:
    -            config.set(args.config_section, override, value)
    +    if args.key:
    +        config.cli_key = args.key
    +    if args.url:
    +        config.cli_url = args.url
     
         return config
     
    
  • wlc/test_base.py+4 2 modified
    @@ -21,7 +21,7 @@
     class ResponseHandler:
         """responses response handler."""
     
    -    def __init__(self, body, filename, auth=False) -> None:
    +    def __init__(self, body: bytes, filename: str, auth: bool = False) -> None:
             """Construct response handler object."""
             self.body = body
             self.filename = filename
    @@ -111,7 +111,9 @@ def format_multipart_body(body, content_type):
             return digest.hexdigest()
     
     
    -def register_uri(path, domain="http://127.0.0.1:8000/api", auth=False) -> None:
    +def register_uri(
    +    path: str, domain: str = "http://127.0.0.1:8000/api", auth: bool = False
    +) -> None:
         """Simplified URL registration."""
         filename = os.path.join(DATA_TEST_BASE, path.replace("/", "-"))
         url = f"{domain}/{path}/"
    
  • wlc/test_data/weblate.ini+3 1 modified
    @@ -1,3 +1,5 @@
     [weblate]
     url = http://127.0.0.1:8000/api/
    -key = KEY
    +
    +[keys]
    +http://127.0.0.1:8000/api/ = KEY
    
  • wlc/test_main.py+4 4 modified
    @@ -91,15 +91,16 @@ def test_config_section(self) -> None:
             self.assertIn("Hello", output)
     
         def test_config_key(self) -> None:
    -        """Configuration using custom config file section and key set."""
    +        """Configuration using custom config file section and key set is ignored."""
             output = self.execute(
                 ["--config", TEST_CONFIG, "--config-section", "withkey", "show", "acl"],
                 settings=False,
    +            expected=1,
             )
    -        self.assertIn("ACL", output)
    +        self.assertIn("Error: You don't have permission to access this object", output)
     
         def test_config_appdata(self) -> None:
    -        """Configuration using custom config file section and key set."""
    +        """Verify keys are loaded from the [keys] section in APPDATA-based config."""
             output = self.execute(["show", "acl"], settings=False, expected=1)
             self.assertIn("You don't have permission to access this object", output)
             try:
    @@ -122,7 +123,6 @@ def test_config_cwd(self) -> None:
         def test_default_config_values(self) -> None:
             """Test default parser values."""
             config = WeblateConfig()
    -        self.assertEqual(config.get("weblate", "key"), "")
             self.assertEqual(config.get("weblate", "retries"), "0")
             self.assertEqual(config.get("weblate", "timeout"), "300")
             self.assertEqual(
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.