T09 · Insecure Skill Coding Practices
- Location
scripts/setup.py:121- Finding
OAuth Credentials and Tokens Stored Without Enforced Access Permissions
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This Trakt skill has a coherent purpose, but it needs Review because setup handles sensitive Trakt credentials with weak storage and unsafe shell/install guidance.
Before installing, treat this as a Review item: use a virtual environment, avoid --break-system-packages, do not grant all Trakt permissions, store ~/.openclaw/trakt_config.json with owner-only permissions, and do not let an agent run the INSTALL.md PIN command through a shell with untrusted input.
scripts/setup.py:121OAuth Credentials and Tokens Stored Without Enforced Access Permissions
scripts/setup.py:110Client Secret Is Collected Through a Terminal-Echoing Prompt
scripts/setup.py:72Setup Recommends Granting All Available Trakt Permissions
scripts/setup.py:47Setup Automatically Modifies the System Python Environment Using an Unbounded Dependency
INSTALL.md:103Unquoted PIN Interpolation Enables Shell Command Injection in the Documented Agent Flow
External input (network, user) flows to a code execution sink. This enables remote code execution or command injection.
# Run auth with PIN
print("\nAuthenticating...")
result = subprocess.run(
[sys.executable, str(CLIENT_SCRIPT), "auth", pin],
capture_output=True,
text=True
Using shell=True for opening URLs on Windows is an unsafe tool invocation pattern because it delegates parsing to the shell. Even with a constant URL today, this is a fragile construct that can become exploitable if any future refactor or environment influence makes the target value attacker-controlled.
elif sys.platform == 'linux':
subprocess.run(['xdg-open', url], check=False)
elif sys.platform == 'win32':
subprocess.run(['start', url], shell=True, check=False)
print_success(f"Opened {url}")
except Exception as e:
print_info(f"Please visit: {url}")
This is a stronger case of tool parameter abuse because shell=True is used with pin_url, which is derived from another process's output and is not validated. If an attacker can influence that output, they may be able to trigger arbitrary shell command execution on Windows during setup.
elif sys.platform == 'linux':
subprocess.run(['xdg-open', pin_url], check=False)
elif sys.platform == 'win32':
subprocess.run(['start', pin_url], shell=True, check=False)
print_success(f"Opened {pin_url}")
except:
print_info(f"Please visit: {pin_url}")
The installation guide directs the agent to execute local shell commands such as dependency checks and package installation, which goes beyond the skill's user-facing purpose of recommendations and watch tracking. Even though the commands shown are specific, permitting an agent to run local commands and launch browser actions from documentation increases attack surface and can normalize unsafe execution behavior.
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
pip3 install requests --break-system-packages
## Step 2: Create Trakt Application
**Action:** Open browser to Trakt application creation page
The guide instructs the agent to solicit a Trakt Client ID and Client Secret from the user and then write them into a local configuration file. While this is part of OAuth-style setup, it expands the agent's authority into credential handling and local persistence, creating unnecessary exposure if the file is readable by other processes or if the agent logs or mishandles the values.
The guide tells the agent to collect a sensitive Client Secret and save it in plaintext under ~/.openclaw/trakt_config.json without discussing sensitivity, permissions, or secure storage. This can lead to credential disclosure through permissive filesystem access, backups, logs, or other local processes, enabling unauthorized use of the Trakt application identity.
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
This will guide you through the entire setup process automatically:
- Install dependencies
- Help you create a Trakt application
- Configure credentials
- Authenticate with PIN
- Test the integration
The README instructs users to store a client secret, access token, and refresh token in a local JSON file but does not warn about the sensitivity of these credentials or recommend secure file permissions. If the local environment is shared, backed up insecurely, or accidentally exposed, these secrets could be used to access the user's Trakt account.
The README says the skill 'automatically triggers when Trakt-related queries are detected' without clearly defining scope or guardrails. In an agent system, ambiguous auto-triggering can cause unintended activation on loosely related prompts, which may expose personal viewing history or invoke authenticated actions unexpectedly.
The skill instructs the agent to read local files, write credential-bearing configuration, access the network, and execute shell commands, but it declares no explicit tool scope or permissions boundary. In an agent environment, this increases the chance of over-privileged execution and makes it harder to constrain or audit what the skill is allowed to do.
The skill establishes persistent local storage for authentication material in ~/.openclaw/trakt_config.json, which creates a session that survives beyond a single interaction. Persistence itself is expected for OAuth integrations, but it becomes a security concern here because long-lived tokens are retained locally without discussion of lifecycle controls, revocation, or storage hardening.
2. **Trakt.tv account** with Pro subscription (required for automatic watch tracking)
3. **Trakt API application** - Create at <https://trakt.tv/oauth/applications>
4. **Configuration file:** `~/.openclaw/trakt_config.json` (see setup below)
The skill directs users to store a Trakt client secret, access token, and refresh token in a plaintext local JSON file without any warning about their sensitivity or guidance on file permissions. If that file is exposed through local compromise, backups, logs, or overly broad agent/file access, an attacker could reuse the credentials to access or manipulate the user's Trakt account.
The authentication section shows client_secret, access_token, and refresh_token values in request and response examples, but provides no caution about secure storage, redaction, or avoiding exposure in logs. For markdown guidance that may be used to implement the skill, omission of any credential-handling warning is a meaningful safety gap.
This markdown file describes calls to /sync/history/{type}/{id} and /recommendations/{type} using Authorization: Bearer <access_token>, which involve sensitive user behavioral data and personalized account information. The documentation does not include any warning that these requests access or transmit private user data to a third-party service.
The setup script automatically installs a package into the user's Python environment using pip and the --break-system-packages flag, which weakens environment isolation and may alter system-managed packages. In an agent skill context, auto-modifying the host environment increases supply-chain and integrity risk, especially if users do not expect setup scripts to perform package installation.
print_info("Installing requests...")
try:
subprocess.run([
sys.executable, "-m", "pip", "install",
"requests", "--break-system-packages"
], check=True, capture_output=True)
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
url = "https://trakt.tv/oauth/applications"
try:
if sys.platform == 'darwin':
subprocess.run(['open', url], check=False)
elif sys.platform == 'linux':
subprocess.run(['xdg-open', url], check=False)
elif sys.platform == 'win32':
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
if sys.platform == 'darwin':
subprocess.run(['open', url], check=False)
elif sys.platform == 'linux':
subprocess.run(['xdg-open', url], check=False)
elif sys.platform == 'win32':
subprocess.run(['start', url], shell=True, check=False)
print_success(f"Opened {url}")
This uses shell=True on Windows to invoke start, which causes shell parsing and creates command-injection risk if the opened target ever becomes attacker-controlled. Even though url is hardcoded here, using this pattern in a setup script normalizes an unsafe execution primitive and increases the blast radius if the value later becomes variable.
elif sys.platform == 'linux':
subprocess.run(['xdg-open', url], check=False)
elif sys.platform == 'win32':
subprocess.run(['start', url], shell=True, check=False)
print_success(f"Opened {url}")
except Exception as e:
print_info(f"Please visit: {url}")
The script writes the Trakt client ID and client secret to a predictable file in the user's home directory without setting restrictive permissions or clearly warning the user about local secret storage. In an agent skill, storing API secrets unencrypted and implicitly can expose credentials to other local users, backup systems, or unrelated processes.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
# Run auth command to get PIN URL
try:
result = subprocess.run(
[sys.executable, str(CLIENT_SCRIPT), "auth"],
capture_output=True,
text=True
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
# Open browser to PIN URL
try:
if sys.platform == 'darwin':
subprocess.run(['open', pin_url], check=False)
elif sys.platform == 'linux':
subprocess.run(['xdg-open', pin_url], check=False)
elif sys.platform == 'win32':
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
if sys.platform == 'darwin':
subprocess.run(['open', pin_url], check=False)
elif sys.platform == 'linux':
subprocess.run(['xdg-open', pin_url], check=False)
elif sys.platform == 'win32':
subprocess.run(['start', pin_url], shell=True, check=False)
print_success(f"Opened {pin_url}")
This combines shell=True with pin_url derived from another program's stdout, creating a credible command-injection path on Windows if the helper script or upstream data can influence that URL. In a skill setup flow, opening attacker-influenced content through a shell is especially dangerous because it can lead to arbitrary command execution on the host.
elif sys.platform == 'linux':
subprocess.run(['xdg-open', pin_url], check=False)
elif sys.platform == 'win32':
subprocess.run(['start', pin_url], shell=True, check=False)
print_success(f"Opened {pin_url}")
except:
print_info(f"Please visit: {pin_url}")
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
# Run auth with PIN
print("\nAuthenticating...")
result = subprocess.run(
[sys.executable, str(CLIENT_SCRIPT), "auth", pin],
capture_output=True,
text=True
No suspicious patterns detected.