Back to skill

Security audit

Github Actions Linter

Security checks for vulnerabilities and agentic risk

Overview

This is a local GitHub Actions linter with some accuracy gaps, but no hidden network access, persistence, or destructive behavior was found.

Reasonable to install for local GitHub Actions linting, but use it as an aid rather than a sole security gate. Prefer pointing it at specific workflow files or .github/workflows, and manually review multiline run blocks, third-party action pinning, and job-level permissions.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/gha_linter.py:454
Finding
Multiline run blocks evade shell-injection and direct-secret checks<![CDATA[ ## Vulnerability Details **File Location**: `scripts/gha_linter.py:454-477` and `scripts/gha_linter.py:539-544` **Vulnerability Type**: Incomplete security analysis of multiline YAML scalar values **Risk Level**: Medium ### Vulnerable Code ```python # shell injection: ${{ }} in run blocks expr_pattern = re.compile(r'\$\{\{.*?\}\}') for i, line in enumerate(lines): stripped = line.strip() # only flag in run: blocks or env values if 'run:' in line or (stripped.startswith('run:') or stripped.startswith('- run:')): exprs = expr_pattern.findall(line) for expr in exprs: inner = expr[3:-2].strip() # check for untrusted contexts for ctx in UNTRUSTED_CONTEXTS: ctx_plain = ctx.replace('*', '') if ctx_plain in inner or (ctx in inner): issues.append(Issue('shell-injection', 'error', f'Expression `{expr}` in run: may be vulnerable to injection via `{ctx}`', i + 1)) break else: # general warning for any expression in run if 'secrets.' not in inner and 'env.' not in inner and 'needs.' not in inner and 'steps.' not in inner and 'matrix.' not in inner and 'inputs.' not in inner: if 'github.event' in inner: issues.append(Issue('untrusted-context', 'warning', f'Expression `{expr}` in run: uses event context — verify it is safe', i + 1)) ``` ```python # secrets directly in run: instead of env: for i, line in enumerate(lines): if 'run:' in line or line.strip().startswith('run:'): if '${{ secrets.' in line: issues.append(Issue('env-in-run', 'warning', f'Secret used directly in `run:` — prefer passing via `env:` for security', i + 1)) ``` ### Technical Analysis The implementation scans individ ...[truncated 2134 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Analyze parsed step objects rather than searching only raw declaration lines. - For every job step with a `run` value, inspect the complete scalar value, including literal and folded multiline blocks. - Preserve source-position metadata for each scalar so findings can identify the exact expression line. - Check every expression in the complete command body against the untrusted-context list. - Detect direct secret interpolation throughout the complete command body. - Recommend assigning expressions to environment variables and referencing safely quoted shell variables. - Add regression tests for `run: |`, `run: >`, multiline commands containing untrusted event fields, and multiline direct secret references. - Where practical, model the selected shell because quoting and metacharacter behavior differ between Bash, PowerShell, and other shells. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/gha_linter.py:523
Finding
Common list-form third-party actions bypass SHA-pinning validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/gha_linter.py:523-536` **Vulnerability Type**: Incorrect parsing of third-party action declarations **Risk Level**: Medium ### Vulnerable Code ```python # third-party actions without SHA pinning for i, line in enumerate(lines): m = re.match(r'\s*uses:\s*([^\s@]+)@(.+)', line.strip()) if m: action = m.group(1) version = m.group(2).strip() # skip official actions/* and docker:// if action.startswith('actions/') or action.startswith('docker://') or action.startswith('./'): continue # check if pinned to SHA (40 hex chars) if not re.match(r'^[0-9a-f]{40}$', version): issues.append(Issue('third-party-action', 'warning', f'Third-party action `{action}@{version}` not pinned to SHA — supply chain risk', i + 1)) ``` ### Technical Analysis GitHub Actions steps normally declare actions using list-item syntax: ```yaml steps: - uses: third-party/example@main ``` The code calls `line.strip()`, producing `- uses: third-party/example@main`. Its regular expression requires the resulting string to begin with `uses:` and does not permit the leading `- ` list marker. Consequently, the ordinary step syntax fails to match and never reaches revision validation. This prevents the linter from warning about mutable branch names, mutable version tags, or other non-SHA references for many third-party actions. ### Attack Path 1. A workflow references a third-party action using normal list syntax, such as `- uses: third-party/example@main`. 2. The action is pinned to a mutable branch or tag rather than an immutable commit SHA. 3. The workflow is checked with the linter's security mode. 4. The line is stripped but retains its YAML list marker. 5. The regular expression fails to match, and no `third-party-action` warning is generated. 6. The action maintainer, a compromised maintainer account, or an attacker wit ...[truncated 516 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Inspect parsed step `uses` properties instead of matching raw YAML lines. - If raw-line matching remains necessary, accept the optional list marker and anchor the complete declaration, for example: ```python m = re.match(r'^(?:-\s*)?uses:\s*([^\s@]+)@([^\s#]+)', line.strip()) ``` - Normalize revision values and require exactly 40 hexadecimal characters for external actions. - Continue handling local actions and Docker references separately. - Consider applying immutable pinning guidance to all remotely hosted actions, including official actions, according to the desired threat model. - Add tests for `- uses: owner/action@main`, version tags, valid uppercase or lowercase commit hashes as appropriate, comments following revisions, and parsed action declarations with additional step properties. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/gha_linter.py:491
Finding
Job-level broad permissions are omitted from the security audit<![CDATA[ ## Vulnerability Details **File Location**: `scripts/gha_linter.py:491-500` **Vulnerability Type**: Incomplete GitHub token permission analysis **Risk Level**: Medium ### Vulnerable Code ```python # permissions check perms = workflow.get('permissions') if perms is None: issues.append(Issue('permissive-permissions', 'info', 'No top-level `permissions` block — defaults to read-write for all scopes', 1)) elif perms == 'write-all': issues.append(Issue('permissive-permissions', 'warning', '`permissions: write-all` grants unnecessary broad access', find_line(lines, 'permissions:'))) ``` ### Technical Analysis The rule examines only the workflow's top-level `permissions` value. GitHub Actions permits each job to override inherited permissions. A workflow can therefore have restrictive top-level permissions while granting `write-all` or multiple writable scopes to an individual job. Because `jobs[*].permissions` is never evaluated, an unsafe job-level override is omitted from the audit. The documented `excessive-permissions` rule is also not implemented in the reviewed security function. ### Attack Path 1. A workflow defines safe top-level permissions, such as `permissions: read-all`. 2. An individual job overrides them with `permissions: write-all` or broad writable scopes. 3. That job processes untrusted input, checks out untrusted code, or invokes a compromised action. 4. The workflow is scanned using the linter. 5. The linter evaluates only the top-level value and emits no warning for the job-level override. 6. Malicious code executes in the affected job and uses its broad `GITHUB_TOKEN` permissions to modify resources accessible to that token. ### Impact Assessment The linter itself does not elevate privileges. An undetected job-level permission grant can increase the impact of a separate workflow compromise. Depending on the granted scopes and repository settings, an attacker may be able to modify reposito ...[truncated 287 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Iterate over every job and evaluate its explicit `permissions` value. - Calculate effective permissions by combining top-level defaults with job-level overrides according to GitHub Actions semantics. - Flag `write-all` at both workflow and job scope. - Validate mapping-form permissions and identify unnecessary `write` grants. - Implement the documented `excessive-permissions` rule with context-aware checks where possible. - Distinguish reusable workflow jobs and account for permission inheritance or reduction. - Report the exact job name and source line for every excessive permission finding. - Add tests covering inherited permissions, restrictive top-level permissions with broad job overrides, explicit empty permissions, mapping-form scopes, and `id-token: write`. ]]>
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (9)

Ae1

High
Category
analysis-evasion
Content
All commands use the bundled Python script at `scripts/gha_linter.py`.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
All commands use the bundled Python script at `scripts/gha_linter.py`.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
All commands use the bundled Python script at `scripts/gha_linter.py`.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
All commands use the bundled Python script at `scripts/gha_linter.py`.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
All commands use the bundled Python script at `scripts/gha_linter.py`.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
All commands use the bundled Python script at `scripts/gha_linter.py`.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill advertises capabilities that imply file reading and possible network access, but it does not declare any explicit tool scope or permission boundaries. In an agent environment, missing scope declarations can let the skill be invoked with broader authority than intended, increasing the risk of unauthorized repository access, data exfiltration, or unsafe outbound requests if the backing implementation evolves or is misconfigured.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The manifest uses broad trigger phrases such as 'check workflow', 'validate CI', and 'workflow issues', which can cause the skill to activate for general workflow-review requests outside its narrow GitHub Actions linting purpose. Over-broad routing increases the chance the agent applies this skill in the wrong context, potentially exposing files unnecessarily or producing misleading security conclusions during unrelated review tasks.

Missing User Warnings

Low
Confidence
77% confidence
Finding
This code reads the contents of user-specified workflow files from disk via `Path(filepath).read_text(...)`, which is a file-access operation on a code file. While the operation is central to linting, there is no user-facing print/log at the point of access and no inline warning or disclosure in the function about reading local files.

Static analysis

No suspicious patterns detected.