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. ]]>
