Back to skill

Security audit

Linear 1.0.0

Security checks for vulnerabilities and agentic risk

Overview

This Linear skill matches its stated purpose, but it uses a write-capable shell wrapper with broad token access and unsafe input handling that could modify the wrong Linear data.

Install only if you are comfortable giving this skill a Linear API key that can read and modify your workspace. Prefer a least-privilege Linear token if available, review commands before running write actions, and avoid passing untrusted issue IDs, titles, descriptions, project names, or comment text until the script uses GraphQL variables and safer cache handling.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/linear.sh:12
Finding
Untrusted CLI arguments are embedded directly into GraphQL, JSON, and jq programs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/linear.sh:12-17, 67-82, 130-145, 161-169, 174-195, 197-263` **Vulnerability Type**: Structured-data injection caused by unsafe string interpolation **Risk Level**: Medium ### Vulnerable Code ```bash gql() { local query="$1" curl -s -X POST "$API" \ -H "Content-Type: application/json" \ -H "Authorization: $LINEAR_API_KEY" \ -d "{\"query\": \"$query\"}" } ``` Representative unescaped query construction: ```bash project_name="${1:-}" if [[ -z "$project_name" ]]; then echo "Usage: linear.sh project <name>" >&2 exit 1 fi gql "{ projects(filter: { name: { containsIgnoreCase: \\\"$project_name\\\" } }, first: 1) { nodes { issues(first: 30, filter: { state: { type: { nin: [\\\"completed\\\", \\\"canceled\\\"] } } }) { nodes { identifier title state { name } priority priorityLabel assignee { name } } } } } }" | format_issues ``` ```bash issue_id="${1:-}" if [[ -z "$issue_id" ]]; then echo "Usage: linear.sh issue <TEAM-123>" >&2 exit 1 fi team_key="${issue_id%%-*}" issue_num="${issue_id##*-}" gql "{ issues(filter: { number: { eq: $issue_num }, team: { key: { eq: \\\"$team_key\\\" } } }) { nodes { identifier title description state { name } priority priorityLabel assignee { name } project { name } team { name } createdAt dueDate } } }" ``` Mutation inputs only escape double quotes: ```bash # Escape quotes in title and description title="${title//\"/\\\"}" description="${description//\"/\\\"}" result=$(gql "mutation { issueCreate(input: { teamId: \\\"$team_id\\\", title: \\\"$title\\\", description: \\\"$description\\\" }) { success issue { identifier title url } } }") ``` The comment command also embeds input into GraphQL and jq source: ```bash body="${body//\"/\\\"}" # First get the issue UUID from the identifier issue_uuid=$(gql "{ issueVcsBranchSearch(branchName: \\\"$issue_id\\\") { id } }" | jq -r '.data.issueVcsBranchSearch.id // empty') if [[ -z "$issue_uuid" ]]; then ...[truncated 3423 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Use GraphQL variables for every external value rather than concatenating values into GraphQL documents. 2. Construct the complete JSON body with `jq -n`, for example: ```bash payload=$(jq -n \ --arg query "$query" \ --arg projectName "$project_name" \ '{query: $query, variables: {projectName: $projectName}}') curl --fail-with-body --silent --show-error \ -X POST "$API" \ -H "Content-Type: application/json" \ -H "Authorization: $LINEAR_API_KEY" \ --data-binary "$payload" ``` 3. Define static GraphQL documents whose structure cannot be influenced by command arguments: ```graphql query ProjectIssues($projectName: String!) { projects(filter: {name: {containsIgnoreCase: $projectName}}, first: 1) { nodes { issues(first: 30) { nodes { identifier title } } } } } ``` 4. Validate structured identifiers before use. For example: - Issue identifiers: `^[A-Za-z][A-Za-z0-9_]*-[0-9]+$` - Team keys: a strict workspace-appropriate allowlist - Issue numbers: `^[0-9]+$` - Status and priority values: explicit case-statement allowlists 5. Never interpolate values into jq source. Use: ```bash jq -r --arg issue_id "$issue_id" \ '.data.issues.nodes[] | select(.identifier == $issue_id) | .id' ``` 6. Check GraphQL `.errors` before consuming `.data`, and verify that UUID lookups return exactly one expected issue. 7. Add regression tests containing quotes, backslashes, newlines, control characters, GraphQL punctuation, and jq metacharacters. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/linear.sh:19
Finding
Predictable shared temporary cache permits symlink attacks and cache poisoning<![CDATA[ ## Vulnerability Details **File Location**: `scripts/linear.sh:19-29` **Vulnerability Type**: Unsafe temporary-file creation and use **Risk Level**: Low ### Vulnerable Code ```bash cache_key="$(printf '%s' "$LINEAR_API_KEY" | cksum | awk '{print $1}')" TEAMS_CACHE="${LINEAR_TEAMS_CACHE:-/tmp/linear-teams-${cache_key}.json}" refresh_teams_cache() { gql "{ teams { nodes { id key name } } }" > "$TEAMS_CACHE" } load_teams() { if [[ ! -f "$TEAMS_CACHE" ]]; then refresh_teams_cache fi cat "$TEAMS_CACHE" } ``` ### Technical Analysis The team cache is stored in the globally shared `/tmp` directory under a stable filename derived from a non-cryptographic 32-bit checksum of the API key. The script opens that path using ordinary shell redirection without: - Secure exclusive creation. - Symlink rejection. - File ownership verification. - Restrictive permission enforcement. - Atomic replacement. - Validation of an operator-supplied `LINEAR_TEAMS_CACHE` path. A local attacker does not necessarily need to recover the API key: the active cache filename may be observable through shared-directory listings or process information. Once known, the attacker can attempt to pre-create or replace the path. The cache contains Linear team IDs, keys, and names. `resolve_team_id` trusts this data when mapping a user-provided team key to the UUID later used by mutation commands. Consequently, cache tampering can influence which team UUID is supplied to issue creation and related operations. Because refresh uses `> "$TEAMS_CACHE"`, a pre-existing symbolic link is followed. If the process has permission to truncate the symlink target, refreshing the cache can overwrite another file available to the victim account. ### Attack Path #### Cache-poisoning path 1. A local attacker observes or determines the cache pathname in `/tmp`. 2. The attacker creates or replaces that path with crafted JSON containing attacker-selected team mappings. 3. The victim runs a comm ...[truncated 1477 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store the cache in a private user cache directory rather than shared `/tmp`: ```bash cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/linear-skill" install -d -m 700 "$cache_dir" TEAMS_CACHE="$cache_dir/teams-${cache_key}.json" ``` 2. Set a restrictive umask before creating cache files: ```bash umask 077 ``` 3. Write to a securely created temporary file in the same directory and atomically rename it: ```bash refresh_teams_cache() { local tmp tmp=$(mktemp "$cache_dir/teams.XXXXXX") trap 'rm -f "$tmp"' RETURN gql "{ teams { nodes { id key name } } }" > "$tmp" jq -e '.data.teams.nodes | type == "array"' "$tmp" >/dev/null chmod 600 "$tmp" mv -f -- "$tmp" "$TEAMS_CACHE" trap - RETURN } ``` 4. Before reading an existing cache, verify that it is a regular file, is not a symbolic link, is owned by the current user, and does not have group or world permissions. 5. If `LINEAR_TEAMS_CACHE` remains configurable, reject unsafe paths or apply the same ownership, type, and permission checks. 6. Validate cached JSON structure and expected field types before using team IDs. 7. Avoid deriving public filenames directly from secret material, even through a weak checksum. Use a non-secret workspace identifier or a keyed cryptographic digest if per-token separation is necessary. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (4)

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill documents and encourages shell execution via `scripts/linear.sh` but does not declare any explicit tool scope such as `permissions` or `allowed-tools`. This creates an authorization gap: an agent or platform may treat the skill as less sensitive than it really is, even though it can query external services and perform state-changing actions like creating issues, commenting, assigning, and changing status.

External Transmission

Medium
Category
Data Exfiltration
Content
# Linear CLI wrapper
# Requires: LINEAR_API_KEY, curl, jq

API="https://api.linear.app/graphql"

if [[ -z "${LINEAR_API_KEY:-}" ]]; then
  echo "Error: LINEAR_API_KEY not set" >&2
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
gql() {
  local query="$1"
  curl -s -X POST "$API" \
    -H "Content-Type: application/json" \
    -H "Authorization: $LINEAR_API_KEY" \
    -d "{\"query\": \"$query\"}"
Confidence
70% 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
93% confidence
Finding
This shell script includes commands that create issues and modify remote Linear data (`create`, `comment`, `status`, `priority`, `assign`) via GraphQL mutations, but there is no confirmation prompt or explicit user-facing warning that these actions will change data in the user's Linear workspace. Although success messages are printed after execution, they do not disclose the safety impact beforehand, and the header comments do not warn that the script performs remote write operations.

Static analysis

No suspicious patterns detected.