T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/mercado_publico_api.py:84
- Finding
- API Ticket Persisted in Plaintext Cache Metadata## Vulnerability Details **File Location**: `scripts/mercado_publico_api.py`, lines 84–88 and 118–126 **Vulnerability Type**: Plaintext persistence of sensitive authentication material **Risk Level**: Medium ### Vulnerable Code ```python def build_url(path: str, ticket: str, params: dict[str, Any]) -> str: clean = {k: str(v) for k, v in params.items() if v is not None and str(v) != ""} clean["ticket"] = ticket query = urllib.parse.urlencode(clean) return f"{API_BASE}{path}?{query}" ``` ```python def _write_cache(cache_path: Path, url: str, payload: Any) -> None: cache_path.parent.mkdir(parents=True, exist_ok=True) data = { "url": _normalize_url_for_cache(url), "fetched_at": int(time.time()), "payload": payload, } tmp_path = cache_path.with_suffix(".tmp") tmp_path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") tmp_path.replace(cache_path) ``` The vulnerable cache write is reached at lines 155–156: ```python payload = fetch_with_retry(url, timeout, max_retries, backoff_seconds) _write_cache(cache_path, url, payload) ``` ### Technical Analysis The script correctly obtains `MERCADO_PUBLICO_API_TICKET` from an environment variable and sends it to the declared official Mercado Público HTTPS API. That network transmission is necessary for the Skill’s read-only API functionality and does not, by itself, indicate unauthorized exfiltration. However, `build_url()` places the ticket in the URL query string. When the optional cache is enabled through `--cache-ttl`, `_write_cache()` stores the complete normalized URL in the cache document. URL normalization sorts and re-encodes the query parameters but does not remove or redact the `ticket` parameter. Consequently, the API ticket is written to disk in recoverable plaintext. The cache destination is also configurable through `--cache-dir`. The implementation creates directories and files without explicitly enforcing owner-only permiss ...[truncated 2194 chars]
- Remediation
- ## Remediation Suggestions 1. **Never store the ticket in cache metadata.** Remove the `ticket` parameter before serializing the URL: ```python def _redact_url(url: str) -> str: parsed = urllib.parse.urlparse(url) pairs = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True) safe_pairs = [ (key, "[REDACTED]" if key.lower() == "ticket" else value) for key, value in pairs ] return urllib.parse.urlunparse( ( parsed.scheme, parsed.netloc, parsed.path, "", urllib.parse.urlencode(safe_pairs), "", ) ) ``` Store `_redact_url(url)` rather than `_normalize_url_for_cache(url)`. 2. **Exclude the credential from cache-key material.** Construct cache keys from the endpoint and non-secret filters only. This avoids unnecessarily processing authentication material as persistent-cache identity. 3. **Separate request authentication from cache identity.** Prefer passing a structured endpoint and parameter dictionary into the cache layer, with the ticket added only at the network-request boundary. 4. **Apply restrictive filesystem permissions.** Create the cache directory with mode `0700` and cache files with mode `0600`, while accounting for cross-platform behavior and existing directories. 5. **Use a safe default cache location.** Prefer a user-private cache directory rather than a project-relative directory that may be shared, archived, or accidentally committed. 6. **Warn about custom cache locations.** Document that `--cache-dir` must not point to shared, synchronized, publicly served, or version-controlled directories. 7. **Remove existing exposed cache entries.** Delete cache files created by affected versions and rotate or revoke any API ticket that may have been stored in a location accessible to untrusted parties. 8. **Add regression tests.** Verify that neither cache file contents nor cache filenames contain the literal ticket and th ...[truncated 61 chars]
