Use with caution. Address findings before production.
Scanned 5/3/2026, 6:25:49 PMยทCached resultยทFast Scanยท88 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
9
high
15
medium
0
low
MCP Server Information
Findings
This package presents significant security concerns with a C-grade and 63/100 safety score, driven primarily by 9 high-severity findings across command injection, prompt injection, and server configuration issues. The 15 medium-severity server configuration problems suggest inadequate security hardening that could expose the system to exploitation. You should address these vulnerabilities before deployment, particularly the command and prompt injection risks which could allow attackers to execute arbitrary code or manipulate the model's behavior.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 24 findings using rule-based analysis. Upgrade for LLM consensus across 5 judges, AI-generated remediation, and cross-file taint analysis.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
24 of 24 findings
24 findings
Command injection risk. Shell-execution sink called with interpolated / attacker-controllable input. Use list-arg subprocess with shell=False, or escape every variable via shlex.quote (Python) / shell-escape (Node).
Evidence
| 153 | // Extract MCP Bundle to test directory |
| 154 | const testDir = 'test-mcpb-client'; |
| 155 | execSync(`rm -rf ${testDir}`); |
| 156 | execSync(`mkdir -p ${testDir} && unzip -q airtable-mcp-server.mcpb -d ${testDir}`); |
| 157 | |
| 158 | // Start the MCP server from the extracted MCP Bundle |
| 159 | const serverProcess = spawn('node', [path.join(testDir, 'dist/main.js')], { |
Remediation
Replace shell-based execution with list-arg subprocess calls (shell=False) or escape every interpolated value with shlex.quote (Python) / shell-escape (Node). Treat every MCP tool input as hostile.
Command injection risk. Shell-execution sink called with interpolated / attacker-controllable input. Use list-arg subprocess with shell=False, or escape every variable via shlex.quote (Python) / shell-escape (Node).
Evidence
| 152 | // Extract MCP Bundle to test directory |
| 153 | const testDir = 'test-mcpb-client'; |
| 154 | execSync(`rm -rf ${testDir}`); |
| 155 | execSync(`mkdir -p ${testDir} && unzip -q airtable-mcp-server.mcpb -d ${testDir}`); |
| 156 | |
| 157 | // Start the MCP server from the extracted MCP Bundle |
Remediation
Replace shell-based execution with list-arg subprocess calls (shell=False) or escape every interpolated value with shlex.quote (Python) / shell-escape (Node). Treat every MCP tool input as hostile.
Command injection risk. Shell-execution sink called with interpolated / attacker-controllable input. Use list-arg subprocess with shell=False, or escape every variable via shlex.quote (Python) / shell-escape (Node).
Evidence
| 166 | () => { |
| 167 | // Clean up test directory |
| 168 | if (fs.existsSync(testDir)) { |
| 169 | execSync(`rm -rf ${testDir}`); |
| 170 | } |
| 171 | }, |
| 172 | ); |
Remediation
Replace shell-based execution with list-arg subprocess calls (shell=False) or escape every interpolated value with shlex.quote (Python) / shell-escape (Node). Treat every MCP tool input as hostile.
Tool 'create_comment' accepts recordId argument and calls createComment with baseId, tableId, recordId as sole filters without verifying caller ownership of the record.
Evidence
| 21 | export function registerCreateComment(server: McpServer, ctx: ToolContext): void { |
| 22 | server.registerTool( |
| 23 | 'create_comment', |
| 24 | { |
| 25 | title: 'Create Comment', |
| 26 | description: 'Create a comment on a record', |
Remediation
Derive the caller's identity from `ctx.principal` (or the MCP auth layer equivalent) and include it in the query's WHERE clause (`... AND owner_id = :user_id`). Raise PermissionError when the row is missing OR not owned by the caller โ never leak the distinction.
Tool 'getRecord' accepts recordId argument and fetches record by baseId, tableId, recordId without verifying caller ownership or access control.
Evidence
| 65 | if (options.view) { |
| 66 | queryParams.append('view', options.view); |
| 67 | } |
| 68 | |
| 69 | if (offset) { |
| 70 | queryParams.append('offset', offset); |
Remediation
Derive the caller's identity from `ctx.principal` (or the MCP auth layer equivalent) and include it in the query's WHERE clause (`... AND owner_id = :user_id`). Raise PermissionError when the row is missing OR not owned by the caller โ never leak the distinction.
Tool 'list_tables' shadows the reserved database tool name from the corpus.
Evidence
| 44 | "description": "List all accessible Airtable bases" |
| 45 | }, |
| 46 | { |
| 47 | "name": "list_tables", |
| 48 | "description": "List all tables in a specific base" |
| 49 | }, |
| 50 | { |
Remediation
Namespace tool names with a server-specific prefix (`acme_read_file` instead of `read_file`). Reserve generic verbs only for servers whose identity the operator has verified. The authoritative reserved-names corpus lives at `_shared/popular-tool-names/v1/names.yaml` โ bump to v2 of the corpus when adding/removing names.
Tool 'describe_table' shadows the reserved database tool name from the corpus.
Evidence
| 48 | "description": "List all tables in a specific base" |
| 49 | }, |
| 50 | { |
| 51 | "name": "describe_table", |
| 52 | "description": "Get detailed information about a specific table" |
| 53 | }, |
| 54 | { |
Remediation
Namespace tool names with a server-specific prefix (`acme_read_file` instead of `read_file`). Reserve generic verbs only for servers whose identity the operator has verified. The authoritative reserved-names corpus lives at `_shared/popular-tool-names/v1/names.yaml` โ bump to v2 of the corpus when adding/removing names.
AirtableService constructor reads AIRTABLE_API_KEY from process.env at module initialization time and caches it as this.apiKey, then uses it for all Airtable API calls without consulting per-request caller identity.
Evidence
| 1 | import {z} from 'zod'; |
| 2 | import { |
| 3 | type IAirtableService, |
| 4 | type ListBasesResponse, |
Remediation
Pull the token / credential from `ctx.principal` (or the MCP auth layer equivalent) on every call. The handler must fail closed when the caller supplies no credential. Never cache a global API token at import time and reuse it across callers.
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; |
| 4 | import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js'; |
| 5 | import express from 'express'; |
| 6 | import {AirtableService} from './airtableService.js'; |
| 7 | import {createServer} from './index.js'; |
| 8 | |
| 9 | function setupSignalHandlers(cleanup: () => Promise<void>): void { |
| 10 | process.on('SIGINT', async () => { |
| 11 | await cleanup(); |
| 12 | process.exit(0); |
| 13 | }); |
| 14 | process.on('SIGTERM', async |
Remediation
Apply auth middleware at the route or router level: - Express / Fastify / Koa: `passport.authenticate(...)`, `requireAuth`, `verifyToken`, or an equivalent JWT middleware applied via `router.use(authMw)` or as a per-route handler. - FastAPI: `Depends(get_current_user)`, `OAuth2PasswordBearer`, `HTTPBearer`, or `verify_jwt` dependency. - Flask: `@login_required`, `@auth_required`, `@jwt_required`, or call `verify_jwt_in_request()` in the handler. Mounting MCP behind a s
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage โ e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile |
| 2 | # Use a Node.js image |
| 3 | FROM node:24-alpine AS builder |
| 4 | |
| 5 | # Set working directory |
| 6 | WORKDIR /app |
| 7 | |
| 8 | # Copy package.json and package-lock.json to the working directory |
| 9 | COPY package.json package-lock.json ./ |
| 10 | |
| 11 | # Install project dependencies |
| 12 | RUN npm install |
| 13 | |
| 14 | # Copy the entire project directory |
| 15 | COPY . . |
| 16 | |
| 17 | # Build the project |
| 18 | RUN npm run build |
| 19 | |
| 20 | # Start a new stage for the final image |
| 21 | FROM node:24-alpine AS release |
| 22 | |
| 23 | # Add MCP re |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # airtable-mcp-server |
| 2 | |
| 3 | A Model Context Protocol server that provides read and write access to Airtable databases. This server enables LLMs to inspect database schemas, then read and write records. |
| 4 | |
| 5 | https://github.com/user-attachments/assets/c8285e76-d0ed-4018-94c7-20535db6c944 |
| 6 | |
| 7 | ## Installation |
| 8 | |
| 9 | **Step 1**: [Create an Airtable personal access token by clicking here](https://airtable.com/create/tokens/new). Details: |
| 10 | - Name: Anything you want e.g. 'Airtable MCP Server Token'. |
| 11 | - Scopes: `schema.base |
Remediation
Declare a real authentication mechanism in the manifest, matching what the running server actually enforces: - `"auth": "bearer"` with a token scheme documented for callers - `"auth": "oauth"` / `"oauth2": { ... }` for delegated flows - `"apiKey": { "header": "X-API-Key", "prefix": "..." }` - `"mtls": true` when client certificates are required If the server is intentionally unauthenticated (stdio-only, local developer tool, trusted-host network), document the assumption in the manifest via a `"
File registers a state-changing HTTP route (POST / PUT / PATCH / DELETE) but no CSRF protection middleware is applied anywhere in the file. If the server uses cookie-based session auth, a cross-site request from any origin can hit this route while the user's cookies ride along. Apply CSRF middleware: - Express: `csurf` / `csrf-csrf` / `lusca.csrf()` - FastAPI: `fastapi-csrf-protect` - Flask: `flask_wtf.csrf.CSRFProtect` Or, if the route is a JSON API authenticated by bearer tokens (no co
Evidence
| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; |
| 4 | import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js'; |
| 5 | import express from 'express'; |
| 6 | import {AirtableService} from './airtableService.js'; |
| 7 | import {createServer} from './index.js'; |
| 8 | |
| 9 | function setupSignalHandlers(cleanup: () => Promise<void>): void { |
| 10 | process.on('SIGINT', async () => { |
| 11 | await cleanup(); |
| 12 | process.exit(0); |
| 13 | }); |
| 14 | process.on('SIGTERM', async |
Remediation
Apply CSRF middleware at the route or router level: - Express: `app.use(csurf())` / `csrf-csrf` package - FastAPI: `fastapi-csrf-protect` with `Depends(...)` - Flask: `CSRFProtect(app)` from `flask_wtf.csrf` Or move to bearer-token auth and set `SameSite=Strict` / `SameSite=Lax` on any session cookies. Document the choice in the project README so reviewers can confirm intent.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 116 | labels: ${{ steps.meta.outputs.labels }} |
| 117 | platforms: linux/amd64,linux/arm64 |
| 118 | - name: Create GitHub Release |
| 119 | uses: softprops/action-gh-release@v2 |
| 120 | with: |
| 121 | files: airtable-mcp-server.mcpb |
| 122 | generate_release_notes: true |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 95 | - name: Set up Docker Buildx |
| 96 | uses: docker/setup-buildx-action@v3 |
| 97 | - name: Login to Docker Hub |
| 98 | uses: docker/login-action@v3 |
| 99 | with: |
| 100 | username: domdomegg |
| 101 | password: ${{ secrets.DOCKER_ACCESS_TOKEN }} |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 108 | type=semver,pattern={{version}} |
| 109 | type=raw,value=latest,enable={{is_default_branch}} |
| 110 | - name: Build and push Docker image |
| 111 | uses: docker/build-push-action@v5 |
| 112 | with: |
| 113 | context: . |
| 114 | push: true |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 101 | password: ${{ secrets.DOCKER_ACCESS_TOKEN }} |
| 102 | - name: Extract Docker metadata |
| 103 | id: meta |
| 104 | uses: docker/metadata-action@v5 |
| 105 | with: |
| 106 | images: domdomegg/airtable-mcp-server |
| 107 | tags: | |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 93 | env: |
| 94 | NODE_AUTH_TOKEN: ${{ steps.npm-token.outputs.token }} |
| 95 | - name: Set up Docker Buildx |
| 96 | uses: docker/setup-buildx-action@v3 |
| 97 | - name: Login to Docker Hub |
| 98 | uses: docker/login-action@v3 |
| 99 | with: |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 20 | - name: Checkout ${{ github.sha }} |
| 21 | uses: actions/checkout@v4 |
| 22 | - name: Use Node.js ${{ matrix.node-version }} |
| 23 | uses: actions/setup-node@v4 |
| 24 | with: |
| 25 | node-version: ${{ matrix.node-version }} |
| 26 | registry-url: https://registry.npmjs.org/ |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 18 | CI: true |
| 19 | steps: |
| 20 | - name: Checkout ${{ github.sha }} |
| 21 | uses: actions/checkout@v4 |
| 22 | - name: Use Node.js ${{ matrix.node-version }} |
| 23 | uses: actions/setup-node@v4 |
| 24 | with: |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 81 | - uses: google-github-actions/auth@v2 |
| 82 | with: |
| 83 | workload_identity_provider: 'projects/457105351064/locations/global/workloadIdentityPools/github-secrets-pool/providers/github-secrets-github' |
| 84 | - uses: google-github-actions/setup-gcloud@v2 |
| 85 | - name: Get NPM token |
| 86 | id: npm-token |
| 87 | run: | |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 62 | - name: Checkout ${{ github.sha }} |
| 63 | uses: actions/checkout@v4 |
| 64 | - name: Use Node.js with the npmjs.org registry |
| 65 | uses: actions/setup-node@v4 |
| 66 | with: |
| 67 | node-version: lts/* |
| 68 | registry-url: https://registry.npmjs.org/ |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 60 | CI: true |
| 61 | steps: |
| 62 | - name: Checkout ${{ github.sha }} |
| 63 | uses: actions/checkout@v4 |
| 64 | - name: Use Node.js with the npmjs.org registry |
| 65 | uses: actions/setup-node@v4 |
| 66 | with: |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 43 | unzip airtable-mcp-server.mcpb -d .github/tmp |
| 44 | - name: Upload MCP Bundle artifact |
| 45 | if: matrix.node-version == 'lts/*' |
| 46 | uses: actions/upload-artifact@v4 |
| 47 | with: |
| 48 | name: airtable-mcp-server-mcpb |
| 49 | path: .github/tmp/* |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable โ a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 78 | MCPB_FILE_SHA256=$(sha256sum airtable-mcp-server.mcpb | cut -d' ' -f1) |
| 79 | sed "s/{{VERSION}}/$VERSION/g; s/{{MCPB_FILE_SHA256}}/$MCPB_FILE_SHA256/g" server.json > server.json.tmp |
| 80 | mv server.json.tmp server.json |
| 81 | - uses: google-github-actions/auth@v2 |
| 82 | with: |
| 83 | workload_identity_provider: 'projects/457105351064/locations/global/workloadIdentityPools/github-secrets-pool/providers/github-secrets-github' |
| 84 | - uses: google-github-actions/setup-gcloud@v2 |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version โ Dependabot can do this automatically with `version-update-strategy: inc