T09 · Insecure Skill Coding Practices
- Location
scripts/mercado_publico_api.py:84- Finding
API Ticket Persisted in Plaintext Cache Metadata
- Content
View full analysis
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: MediumVulnerable 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_TICKETfrom 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 theticketparameter. 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
View remediation
Remediation Suggestions
- Never store the ticket in cache metadata. Remove the
ticketparameter 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).-
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.
-
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.
-
Apply restrictive filesystem permissions. Create the cache directory with mode
0700and cache files with mode0600, while accounting for cross-platform behavior and existing directories. -
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.
-
Warn about custom cache locations. Document that
--cache-dirmust not point to shared, synchronized, publicly served, or version-controlled directories. -
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.
-
Add regression tests. Verify that neither cache file contents nor cache filenames contain the literal ticket and th ...[truncated 61 chars]
- Never store the ticket in cache metadata. Remove the
