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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

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

Medium
Category
Data Flow
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.

Content

Scanner excerpt · scripts/get_timeline.py (reported line 37)May include surrounding context.

python
# 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()}

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

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

Medium
Category
Data Flow
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.

Content

Scanner excerpt · scripts/get_user_tweets.py (reported line 40)May include surrounding context.

python
# 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()}

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/get_timeline.py (reported line 31)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/get_timeline.py (reported line 36)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/get_user_tweets.py (reported line 31)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/get_user_tweets.py (reported line 39)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/like_tweet.py (reported line 28)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/like_tweet.py (reported line 35)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/post_tweet.py (reported line 53)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/reply_tweet.py (reported line 28)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/retweet.py (reported line 29)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/retweet.py (reported line 34)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/search_tweets.py (reported line 33)May include surrounding context.

python
import requests
    
    url = "https://api.twitter.com/2/tweets"
    headers = {
        "Authorization": f"Bearer {creds['bearer_token']}",
        "Content-Type": "application/json"

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/like_tweet.py (reported line 43)May include surrounding context.

python
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}

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/post_tweet.py (reported line 61)May include surrounding context.

python
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}

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/reply_tweet.py (reported line 39)May include surrounding context.

python
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}