Back to skill

Security audit

ClawRank

Security checks for vulnerabilities and agentic risk

Overview

The skill is broadly coherent for a leaderboard uploader, but auto-setup can send your GitHub CLI token to ClawRank and it automatically gathers local and GitHub activity with limited user control.

Review this skill before installing. Only use it if you are comfortable sending aggregate OpenClaw usage, GitHub username, and automatic GitHub activity metrics to ClawRank. Avoid auto-setup unless you trust ClawRank with your GitHub CLI token, prefer a manually generated ClawRank token, do not use untrusted endpoint overrides, and be deliberate before enabling the recurring daily job.

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 (3)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/ingest.py:976
Finding
Raw GitHub Access Token Disclosed to a Third-Party Service During Automatic Setup<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ingest.py:976-1027` and `scripts/ingest.py:1105-1114` **Vulnerability Type**: Credential disclosure and excessive privilege acquisition **Risk Level**: Critical ### Vulnerable Code ```python def _get_gh_token() -> str | None: """Get the current GitHub auth token from gh CLI.""" try: result = subprocess.run( ["gh", "auth", "token"], capture_output=True, text=True, timeout=5, ) token = result.stdout.strip() if token and result.returncode == 0: return token except (FileNotFoundError, subprocess.TimeoutExpired, OSError): pass return None CLI_AUTH_PATH = "/api/auth/cli" def run_setup(endpoint: str, verbose: bool = False) -> str | None: """ Auto-setup: exchange a GitHub token for a ClawRank API token. Returns the raw cr_live_ token on success, or None on failure. """ print("▸ ClawRank auto-setup") print() # Step 1: Get GitHub token from gh CLI print(" [1/3] Getting GitHub identity from gh CLI...") gh_token = _get_gh_token() if not gh_token: print(" ✗ Could not get GitHub token. Make sure gh CLI is installed and authenticated.", file=sys.stderr) print(" Run: gh auth login", file=sys.stderr) return None if verbose: print(f" Got GitHub token ({gh_token[:8]}...)") # Step 2: Exchange for ClawRank API token print(" [2/3] Registering with ClawRank...") url = f"{endpoint}{CLI_AUTH_PATH}" body = json.dumps({"githubToken": gh_token, "label": "auto-setup"}).encode("utf-8") req = urllib.request.Request( url, data=body, headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read().decode("utf-8")) ``` Automatic invocation occurs when the ClawRank token is absent: ``` ...[truncated 2956 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `_get_gh_token()` and never invoke `gh auth token`. 2. Never send an existing GitHub CLI credential to ClawRank or any other service. 3. Implement a standard GitHub OAuth web or device authorization flow: - Register a dedicated OAuth application. - Request only the minimum identity scopes required. - Display the requested scopes and obtain explicit user consent. - Exchange authorization codes directly through the documented OAuth protocol. 4. If only account identity is needed, use a narrowly scoped identity assertion rather than repository-authorized credentials. 5. Make setup an explicit action instead of automatically initiating credential-bearing authentication when a token is absent. 6. Clearly disclose the recipient, data fields, purposes, retention period, and requested permissions before authorization. 7. Revoke any GitHub credentials previously transmitted through this mechanism and instruct affected users to rotate them. 8. Store the resulting ClawRank token in an operating-system credential store or a file created with owner-only permissions rather than placing it in a general plaintext configuration file. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/ingest.py:52
Finding
Credential-Bearing Requests Can Be Redirected to Arbitrary or Plaintext Endpoints<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ingest.py:52-55`, `scripts/ingest.py:950-966`, and `scripts/ingest.py:1015-1027` **Vulnerability Type**: Unvalidated network destination for sensitive data **Risk Level**: High ### Vulnerable Code ```python def get_endpoint(override: str | None = None) -> str: if override: return override.rstrip("/") return os.environ.get("CLAWRANK_ENDPOINT", DEFAULT_ENDPOINT).rstrip("/") ``` The selected endpoint receives the ClawRank bearer token: ```python def submit(endpoint: str, token: str, submission: dict) -> dict: """POST a DailyFactSubmission to the ClawRank API. Returns response JSON.""" url = f"{endpoint}{SUBMIT_PATH}" body = json.dumps(submission).encode("utf-8") req = urllib.request.Request( url, data=body, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {token}", }, method="POST", ) try: with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode("utf-8")) ``` The same endpoint receives the raw GitHub token during setup: ```python url = f"{endpoint}{CLI_AUTH_PATH}" body = json.dumps({"githubToken": gh_token, "label": "auto-setup"}).encode("utf-8") req = urllib.request.Request( url, data=body, headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read().decode("utf-8")) ``` ### Technical Analysis The destination can be overridden through the `--endpoint` argument or `CLAWRANK_ENDPOINT` environment variable. The code performs no URL scheme validation, exact-host validation, port restriction, or destination allowlisting. As a result, the script accepts attacker-controlled URLs and permits plaintext HTTP. Two distinct credentials can be exposed: - The raw GitHub token sent in the setup request body. ...[truncated 1747 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove endpoint overrides from production builds and pin requests to `https://clawrank.dev`. 2. Parse destinations with `urllib.parse.urlparse()` and enforce: - The `https` scheme. - The exact expected hostname. - An expected port or the default HTTPS port. - Rejection of embedded credentials, fragments, malformed hosts, and unexpected subdomains. 3. Do not rely only on suffix matching, because domains such as `clawrank.dev.attacker.example` would bypass weak checks. 4. Disable automatic redirects for credential-bearing requests or revalidate every redirect destination before following it. 5. Never transmit a raw GitHub credential, even to an allowlisted endpoint. 6. If custom endpoints are required for development, place them behind a clearly named unsafe-development flag, prevent use with production credentials, and display an explicit warning. 7. Add tests confirming rejection of HTTP, localhost where inappropriate, IP-literal destinations, deceptive hostnames, and unexpected ports. 8. Rotate credentials that may have been submitted to untrusted endpoints. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/ingest.py:228
Finding
Automatic Enumeration and Submission of Private GitHub Activity Exceeds Core Skill Requirements<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ingest.py:228-255` and `scripts/ingest.py:1137-1155` **Vulnerability Type**: Excessive authenticated data access and privacy leakage **Risk Level**: Medium ### Vulnerable Code Repository discovery requests all repositories accessible to the authenticated user: ```python def _discover_repos(login: str, verbose: bool = False) -> list[dict]: """ Discover repos accessible to the authenticated user that have recent pushes. Returns list of {owner, name, full_name, pushed_at}. """ repos = _gh_api("/user/repos?sort=pushed&per_page=100&type=all", paginate=True) if not isinstance(repos, list): return [] # Filter to repos with a push in the last 90 days cutoff = datetime.now(timezone.utc) - timedelta(days=90) results = [] for repo in repos: pushed_at = repo.get("pushed_at", "") try: pushed_at_dt = datetime.fromisoformat(pushed_at.replace("Z", "+00:00")) except (AttributeError, ValueError): continue if pushed_at_dt >= cutoff: owner = repo.get("owner", {}).get("login", "") name = repo.get("name", "") if owner and name: results.append({ "owner": owner, "name": name, "full_name": f"{owner}/{name}", "pushed_at": pushed_at, }) ``` Collection runs automatically whenever the GitHub CLI is authenticated: ```python git_metrics: dict[str, dict] | None = None last_git_sync_date = state.get("lastGitSyncDate") if _gh_available() and gh_username: print(" [git-metrics] gh CLI detected — collecting GitHub commit & PR metrics...") try: git_metrics = collect_github_metrics( login=gh_username, last_submission_date=last_git_sync_date, verbose=args.verbose, ) if git_metrics: total_git_commits = sum(v ...[truncated 2736 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Disable GitHub metric collection by default. 2. Require an explicit option such as `--github-metrics` before making any authenticated GitHub API calls. 3. Present a clear consent notice describing: - Which repositories will be accessed. - Which metrics will be derived. - Which fields will be transmitted. - The destination and retention purpose. 4. Default to public repositories only. 5. Support a user-defined repository allowlist and exclude private or organization repositories unless individually selected. 6. Request and use the narrowest possible GitHub permissions. 7. Provide a dry-run payload preview before first submission. 8. Add a separate option to disable GitHub username transmission. 9. Avoid merging the same account-wide GitHub aggregates into every agent submission unless that association is intentional and explicitly approved. 10. Document how users can revoke authorization and delete previously submitted GitHub-derived metrics. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (18)

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill invokes a Python script that can read local agent data, write configuration files, use network access, and potentially register cron jobs, yet the manifest declares no explicit tool scope or permissions. That creates an over-privileged execution surface where an agent may run sensitive operations without clear user-visible boundaries or policy enforcement.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger text uses broad phrases like submit, sync, report, upload stats, get ranked, and setup automated ingestion, which can cause the skill to activate in ordinary conversation without sufficiently specific user intent. In this context, accidental activation is risky because execution may transmit local usage data and GitHub-derived activity to an external service.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill presents a one-command quick start and convenience messaging before clearly warning that it will scan local session transcripts, derive usage metrics, collect GitHub activity, and send the results to a third-party public leaderboard. Users may consent to 'get ranked' without understanding the scope of local data processing, account linkage, persistence of API tokens, and public exposure of activity-derived metadata.

Ssd 3

Medium
Confidence
96% confidence
Finding
The skill instructs the agent to scan local transcripts and recently active repositories, aggregate metrics, and submit them to a public leaderboard while framing the process as routine productivity reporting. This is dangerous because local transcripts and repository activity can reveal sensitive behavioral, project, and operational metadata, and the public leaderboard context materially increases privacy and confidentiality risk.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _get_gh_username() -> str | None:
    """Extract GitHub username from `gh auth status` output."""
    try:
        result = subprocess.run(
            ["gh", "auth", "status"],
            capture_output=True, text=True, timeout=5,
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _get_git_first_name() -> str | None:
    """Get first name from git config. Returns None if not set or looks like an email."""
    try:
        result = subprocess.run(
            ["git", "config", "--global", "user.name"],
            capture_output=True, text=True, timeout=5,
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
Returns empty dict if the CLI is unavailable or fails.
    """
    try:
        result = subprocess.run(
            ["openclaw", "agents", "list", "--json"],
            capture_output=True, text=True, timeout=10,
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Description-Behavior Mismatch

Medium
Confidence
98% confidence
Finding
The skill claims to report local OpenClaw token usage, but it also enumerates accessible GitHub repositories and uploads commit counts, PR activity, and line-change metrics. This is a material expansion of collected telemetry beyond the stated purpose, increasing privacy and metadata-exfiltration risk for users who may not expect repository activity profiling.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _gh_available() -> bool:
    """Check if gh CLI is installed and authenticated."""
    try:
        result = subprocess.run(
            ["gh", "auth", "status"],
            capture_output=True, text=True, timeout=10,
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if paginate:
        cmd.append("--paginate")
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
        if result.returncode != 0:
            return None
        text = result.stdout.strip()
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _openclaw_available() -> bool:
    """Check if openclaw CLI is installed."""
    try:
        result = subprocess.run(
            ["openclaw", "--version"],
            capture_output=True, text=True, timeout=5,
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _find_cron_job_id() -> str | None:
    """Return the job ID of the clawrank-ingest cron job, or None if not found."""
    try:
        result = subprocess.run(
            ["openclaw", "cron", "list", "--json"],
            capture_output=True, text=True, timeout=15,
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
]

    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
        if result.returncode == 0:
            # Parse job ID from JSON response for the removal hint
            job_id = None
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _get_gh_token() -> str | None:
    """Get the current GitHub auth token from gh CLI."""
    try:
        result = subprocess.run(
            ["gh", "auth", "token"],
            capture_output=True, text=True, timeout=5,
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
The auto-setup flow obtains a GitHub authentication token, sends it to a remote service for exchange, and persists the returned API token into local OpenClaw configuration. This goes well beyond passive usage-stat submission and materially changes trust boundaries by handing a powerful credential to an external endpoint and modifying local auth config automatically.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
At this point the script packages the user's GitHub auth token and transmits it to the remote ClawRank API, but the operation site does not present a strong warning or confirmation that a GitHub credential is leaving the machine. If the remote service is compromised, malicious, or misconfigured, the token could be abused to access repositories and account data permitted by that credential.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The setup flow silently writes a new API token into `~/.openclaw/openclaw.json` without an explicit confirmation step. Persisting credentials automatically can surprise users, increase the blast radius if local files are exposed, and make later automated submissions occur under a token the user did not knowingly store.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
When `--recurring` is used, the script creates a persistent daily cron-style OpenClaw job that continues submitting data in the future. Even though this is gated by a flag, the operation modifies persistent automation and should be treated as a sensitive state change requiring explicit disclosure of schedule, command, and ongoing data transmission.

Static analysis

No suspicious patterns detected.