T09 · Insecure Skill Coding Practices
- Location
scripts/keychain_helper.py:14- Finding
Path Traversal in Secret File Fallback Allows Arbitrary Local File Disclosure
- Content
View full analysis
Vulnerability Details
File Location:
scripts/keychain_helper.py:14-30; attacker-controlled input originates atscripts/get_secret.py:21
Vulnerability Type: Path traversal and arbitrary file read
Risk Level: HighVulnerable Code
scripts/get_secret.py:21:python service = sys.argv[1]scripts/keychain_helper.py:14-30:python def get_secret(service, account=None, secrets_dir=None): """Get a secret from keychain. Falls back to secrets/ file.""" acct = account or ACCOUNT sdir = secrets_dir or SECRETS_DIR try: import keyring value = keyring.get_password(service, acct) if value: return value except Exception: pass # Fallback: read from file path = os.path.join(sdir, service) try: with open(path) as f: return f.read().strip() except Exception: return ""Technical Analysis
The command-line
serviceargument is passed directly toos.path.join()without validating that it is a simple service name. Absolute paths cause Python to discard the intended secrets-directory prefix, while../components can traverse outside that directory.When the corresponding Keychain lookup returns no value or raises an exception, the helper silently opens the constructed path. The CLI then prints the returned contents to standard output. Catching all Keychain exceptions makes the filesystem fallback especially easy to reach and hides the reason that the security boundary was bypassed.
Attack Path
- An attacker able to control arguments passed to
get_secret.pysupplies an absolute path or traversal sequence as the service name. - For example, the attacker requests a value such as
../../../.ssh/id_rsa. - The Keychain lookup does not return a matching item.
os.path.join(sdir, service)resolves outside~/.openclaw/secrets.- The helper read ...[truncated 686 chars]
- An attacker able to control arguments passed to
- Remediation
View remediation
Remediation Suggestions
- Require service identifiers to match a restrictive allowlist, such as
^[A-Za-z0-9._-]+$. - Reject absolute paths, path separators, null characters,
.components, and..components. - Resolve both the configured directory and candidate path, then verify that the candidate's parent is the resolved secrets directory.
- Consider replacing arbitrary service names with an explicit map of permitted service identifiers to files.
- Do not silently fall back after every Keychain exception. Distinguish “item not found” from access-control or backend failures.
- Return structured errors rather than an empty string so callers can detect and handle security failures.
Example containment logic:
python from pathlib import Path import re if not re.fullmatch(r"[A-Za-z0-9._-]+", service): raise ValueError("Invalid service name") base = Path(sdir).expanduser().resolve() candidate = (base / service).resolve() if candidate.parent != base: raise ValueError("Service path escapes secrets directory")- Require service identifiers to match a restrictive allowlist, such as
