Back to skill

Security audit

Storyclaw X Manager

Security checks for vulnerabilities and agentic risk

Overview

This X/Twitter manager is transparent about account-control features, but it stores reusable credentials in plaintext and lets callers choose credential files with an unchecked user ID before public account actions.

Review carefully before installing. Use only in a trusted single-user environment unless credential loading is hardened, keep tokens outside the package or in a secure secret store, restrict file permissions, rotate any exposed tokens, and require explicit confirmation before posting, replying, liking, retweeting, or enabling automation.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (2)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/post_tweet.py:14
Finding
Unvalidated User Identifier Allows Cross-Account Credential Selection and Path Traversal## Vulnerability Details **File Location**: `scripts/get_timeline.py:10-16`, `scripts/get_user_tweets.py:10-16`, `scripts/like_tweet.py:10-16`, `scripts/post_tweet.py:14-21`, `scripts/reply_tweet.py:10-16`, `scripts/retweet.py:10-16`, and `scripts/search_tweets.py:11-17` **Vulnerability Type**: Improper access control and path traversal **Risk Level**: High ### Vulnerable Code The following representative implementation appears in `scripts/post_tweet.py:14-21`: ```python def load_credentials(user_id): """Load Twitter credentials for user""" cred_path = os.path.join(SKILL_DIR, 'credentials', f'{user_id}.json') if not os.path.exists(cred_path): raise Exception(f"No credentials found for user {user_id}. Configure Twitter credentials first.") with open(cred_path, 'r') as f: creds = json.load(f) ``` The same credential-path construction pattern is repeated across all seven scripts: ```python cred_path = os.path.join(SKILL_DIR, 'credentials', f'{user_id}.json') ``` ### Technical Analysis The caller directly controls `user_id`, which is interpolated into a filesystem path without format validation, canonicalization, or a containment check. There is also no authorization check that binds the requested `user_id` to the authenticated caller. This creates two related access-control weaknesses: 1. A caller who knows another user's identifier can select that user's credential file directly. 2. Path components such as `..` can escape the intended `credentials` directory. Because the implementation appends `.json`, the target must be a reachable JSON file with a compatible `twitter` object. The credentials are subsequently used to call X APIs. Although the scripts do not print the credentials directly, an attacker can potentially use another account's credentials indirectly through the supported read and write operations. ### Attack Path 1. The attacker obtains the ability to invok ...[truncated 1513 chars]
Remediation
## Remediation Suggestions 1. Validate `user_id` against a strict allowlist before using it in a path. For example, permit only the exact identifier format required by the application: ```python import re if not re.fullmatch(r'[A-Za-z0-9_-]+', user_id): raise ValueError("Invalid user identifier") ``` 2. Resolve and verify the canonical credential path: ```python credentials_dir = os.path.realpath(os.path.join(SKILL_DIR, "credentials")) cred_path = os.path.realpath( os.path.join(credentials_dir, f"{user_id}.json") ) if os.path.commonpath([credentials_dir, cred_path]) != credentials_dir: raise ValueError("Credential path escapes the credential directory") ``` 3. Do not treat a caller-supplied account identifier as proof of authorization. Obtain the user identity from a trusted authentication context and map it internally to the appropriate credential record. 4. Centralize credential loading in one hardened module rather than duplicating the vulnerable implementation across every script. 5. Use opaque internal credential-record identifiers where practical, and ensure that a caller can access only records explicitly assigned to that caller. 6. Add tests covering absolute paths, `..` traversal, encoded separators, symbolic links, malformed identifiers, and attempts to select another user's credentials. 7. After remediation, review invocation logs for suspicious user identifiers and rotate tokens if cross-account use may already have occurred.

T09 · Insecure Skill Coding Practices

Warning
Location
SKILL.md:25
Finding
Long-Lived X Credentials Are Stored in Plaintext Files Without Enforced Access Controls## Vulnerability Details **File Location**: `SKILL.md:25-36`; credential-loading implementations in `scripts/get_timeline.py:10-21`, `scripts/get_user_tweets.py:10-21`, `scripts/like_tweet.py:10-21`, `scripts/post_tweet.py:14-26`, `scripts/reply_tweet.py:10-21`, `scripts/retweet.py:10-21`, and `scripts/search_tweets.py:11-22` **Vulnerability Type**: Plaintext storage of sensitive authentication material **Risk Level**: Medium ### Vulnerable Configuration `SKILL.md:25-36` specifies plaintext per-user credential files: ```json { "twitter": { "api_key": "", "api_secret": "", "access_token": "", "access_token_secret": "", "bearer_token": "" } } ``` The scripts read those files directly. For example, `scripts/post_tweet.py:14-26` contains: ```python def load_credentials(user_id): """Load Twitter credentials for user""" cred_path = os.path.join(SKILL_DIR, 'credentials', f'{user_id}.json') if not os.path.exists(cred_path): raise Exception(f"No credentials found for user {user_id}. Configure Twitter credentials first.") with open(cred_path, 'r') as f: creds = json.load(f) if 'twitter' not in creds: raise Exception("Twitter credentials not found. Please bind X account at storyclaw.com") return creds['twitter'] ``` ### Technical Analysis The documented architecture stores reusable API keys, API secrets, access tokens, access-token secrets, and bearer tokens as unencrypted JSON inside the project directory. The implementation does not enforce file ownership, reject permissive file modes, encrypt credential records, or retrieve secrets from a dedicated secret-management service. The included `credentials/example.json` contains only empty placeholders and therefore does not itself expose live credentials. The risk arises when users follow the documented architecture and populate similarly structured files with operational secrets. ...[truncated 1315 chars]
Remediation
## Remediation Suggestions 1. Store credentials in an operating-system keychain, managed secret service, hardware-backed vault, or encrypted application credential store instead of plaintext project files. 2. If file-based storage is unavoidable: - Place credentials outside the source and installation directories. - Set the credential directory to owner-only access, such as mode `0700`. - Set each credential file to owner read/write only, such as mode `0600`. - Verify file ownership and permissions before reading. - Reject symbolic links, non-regular files, and files owned by unexpected users. - Encrypt records at rest using a key that is not stored beside the encrypted files. 3. Add `credentials/*.json` to source-control and packaging exclusions while retaining only a non-sensitive example template. 4. Use separate, least-privilege tokens for read and write operations where supported. Avoid granting write access to tokens used only for timeline or search operations. 5. Avoid including secrets or upstream response bodies containing sensitive information in exceptions, logs, telemetry, or command output. 6. Implement token rotation and revocation procedures. Rotate any token that may have entered source control, an archive, a shared backup, or a broadly readable filesystem location. 7. Document secure provisioning requirements and fail closed when credential-file ownership or permissions are unsafe.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (27)

Missing User Warnings

High
Confidence
98% confidence
Finding
The skill instructs storing per-user Twitter credentials in local JSON files, including API secrets and access tokens, without warning about sensitivity, access controls, encryption, or lifecycle handling. Plain local storage of reusable credentials materially raises the risk of account takeover if the host, logs, backups, or neighboring processes can access those files.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill declares capabilities that imply file access and network use, but it does not specify any explicit tool scope or permission boundaries. For a skill that can read local credential files and perform live Twitter/X actions over the network, this lack of scoping increases the chance of overbroad execution, unintended data access, or unauthorized external actions.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The description is broad enough to permit many different account-management behaviors without clear invocation constraints or safety gates. In a skill that can post, reply, retweet, and automate engagement, vague triggering increases the risk of accidental activation, misuse for spammy behavior, or actions being taken without sufficiently specific user intent.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The skill supports live posting and automated engagement on behalf of a user but does not clearly warn that it can take externally visible actions against a real social-media account. In this context, the omission is dangerous because users may not appreciate that commands can cause irreversible public posts, replies, likes, retweets, or automated interactions that affect reputation, compliance, or platform standing.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This code loads stored Twitter credentials from a local credentials file and uses the bearer token to make outbound requests to the Twitter API. Aside from a minimal module docstring, there is no confirmation prompt, user-facing notice, or explanatory comment disclosing that the script accesses credentials and transmits authenticated account data over the network.

Tainted flow: 'url' from requests.get (line 36, network input) → requests.get (network output)

Medium
Category
Data Flow
Content
# Get timeline
    url = f"https://api.twitter.com/2/users/{user_id_twitter}/timelines/reverse_chronological?max_results={count}"
    response = requests.get(url, headers=headers)
    
    if response.status_code == 200:
        return {"success": True, "tweets": response.json()}
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The script loads stored Twitter bearer credentials from disk and automatically transmits them to an external service without any explicit consent, disclosure, or scoping checks at execution time. In an agent skill context, this increases the risk of silent credential use and unintended access to third-party accounts or APIs, especially if the caller can supply arbitrary user_id values.

Tainted flow: 'tweets_url' from requests.get (line 39, network input) → requests.get (network output)

Medium
Category
Data Flow
Content
# Get tweets
    tweets_url = f"https://api.twitter.com/2/users/{twitter_user_id}/tweets?max_results={count}"
    response = requests.get(tweets_url, headers=headers)
    
    if response.status_code == 200:
        return {"success": True, "tweets": response.json()}
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
This code loads Twitter credentials from a local credentials file and sends authenticated HTTP requests to Twitter to like a tweet, but it provides no confirmation prompt, user-facing notice, or explanatory comment beyond the minimal docstring. Because the operation uses stored credentials and performs an external account action on the user's behalf, some visible disclosure is warranted in the code when no accompanying markdown warning is present in this file.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
This code performs an external HTTP POST that transmits user-provided content and account-linked authorization data to Twitter, but there is no confirmation prompt or user-visible notice immediately before the action. The minimal module docstring only says 'Reply to a tweet' and does not warn that running the script will publish content on the user's behalf.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
This script performs a state-changing social media action using stored bearer-token credentials with no confirmation, approval gate, or meaningful disclosure to the operator. In the context of an agent skill that can automate X/Twitter activity, this increases the risk of unauthorized or unintended retweets that can damage the user's account reputation, spread harmful content, or be abused by a higher-level agent prompt flow.

External Transmission

Medium
Category
Data Exfiltration
Content
url = f"https://api.twitter.com/2/users/{user_id_twitter}/retweets"
    data = {"tweet_id": tweet_id}
    
    response = requests.post(url, headers=headers, json=data)
    
    if response.status_code == 200:
        return {"success": True}
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
url = f"https://api.twitter.com/2/users/{user_id_twitter}/retweets"
    data = {"tweet_id": tweet_id}
    
    response = requests.post(url, headers=headers, json=data)
    
    if response.status_code == 200:
        return {"success": True}
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
url = f"https://api.twitter.com/2/users/{user_id_twitter}/retweets"
    data = {"tweet_id": tweet_id}
    
    response = requests.post(url, headers=headers, json=data)
    
    if response.status_code == 200:
        return {"success": True}
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.