Use with caution. Address findings before production.
Scanned 5/4/2026, 5:05:16 AMยทCached resultยทDeep Scanยท88 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
10
high
15
medium
0
low
MCP Server Information
Findings
This package presents significant security concerns with a C grade and safety score of 62/100, driven primarily by 10 high-severity findings across command injection, prompt injection, and server configuration issues. The 15 medium-severity findings related to server configuration suggest improper setup that could expose the system to attacks or misuse. 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 server's behavior.
AIPer-finding remediation generated by bedrock-claude-haiku-4-5 โ 25 of 25 findings. Click any finding to read.
No known CVEs found for this package or its dependencies.
Scan Details
Done
Sign in to save scan history and re-scan automatically on new commits.
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.
25 of 25 findings
25 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
| 154 | // Extract MCP Bundle to test directory |
| 155 | const testDir = 'test-mcpb-client'; |
| 156 | execSync(`rm -rf ${testDir}`); |
| 157 | execSync(`mkdir -p ${testDir} && unzip -q airtable-mcp-server.mcpb -d ${testDir}`); |
| 158 | |
| 159 | // Start the MCP server from the extracted MCP Bundle |
RemediationAI
The problem is that `execSync()` with template literals interpolates `testDir` directly into the shell command, allowing command injection if `testDir` is attacker-controlled. Replace both `execSync()` calls with the `fs` module: use `fs.rmSync(testDir, { recursive: true, force: true })` instead of `execSync('rm -rf ...')` and `fs.mkdirSync(testDir, { recursive: true })` plus a separate `unzip` library call or `child_process.execFile()` with array arguments. This eliminates shell interpretation entirely. Verify by confirming the test directory is created and cleaned up correctly without spawning a shell process.
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
| 155 | // Extract MCP Bundle to test directory |
| 156 | const testDir = 'test-mcpb-client'; |
| 157 | execSync(`rm -rf ${testDir}`); |
| 158 | execSync(`mkdir -p ${testDir} && unzip -q airtable-mcp-server.mcpb -d ${testDir}`); |
| 159 | |
| 160 | // Start the MCP server from the extracted MCP Bundle |
| 161 | const serverProcess = spawn('node', [path.join(testDir, 'dist/main.js')], { |
RemediationAI
The problem is that `execSync()` with template literals interpolates `testDir` directly into the shell command, allowing command injection if `testDir` is attacker-controlled. Replace the `execSync()` call with `fs.mkdirSync(testDir, { recursive: true })` and use `child_process.execFile('unzip', ['-q', 'airtable-mcp-server.mcpb', '-d', testDir])` to pass arguments as an array without shell interpretation. This eliminates shell interpretation and prevents injection. Verify by confirming the MCP bundle is extracted to the test directory without spawning a shell process.
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
| 168 | () => { |
| 169 | // Clean up test directory |
| 170 | if (fs.existsSync(testDir)) { |
| 171 | execSync(`rm -rf ${testDir}`); |
| 172 | } |
| 173 | }, |
| 174 | ); |
RemediationAI
The problem is that `execSync()` with template literals interpolates `testDir` directly into the shell command, allowing command injection if `testDir` is attacker-controlled. Replace `execSync('rm -rf ${testDir}')` with `fs.rmSync(testDir, { recursive: true, force: true })` to use the filesystem API directly without shell interpretation. This eliminates shell interpretation entirely. Verify by confirming the test directory is removed correctly during cleanup without spawning a shell process.
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', |
RemediationAI
The problem is that the `create_comment` tool accepts a `recordId` parameter and calls the Airtable API without verifying that the caller owns or has access to that record. Add an ownership/permission check before calling `createComment()`: query the record's metadata or use Airtable's sharing/permissions API to confirm the authenticated caller has write access to the record in the specified base and table. This prevents unauthorized users from creating comments on records they do not own. Verify by attempting to create a comment on a record from a different user's base and confirming the request is rejected.
Tool 'getRecord' accepts recordId argument and fetches record by baseId, tableId, recordId without ownership verification.
Evidence
| 65 | if (options.view) { |
| 66 | queryParams.append('view', options.view); |
| 67 | } |
| 68 | |
| 69 | if (offset) { |
| 70 | queryParams.append('offset', offset); |
RemediationAI
The problem is that the `getRecord` tool accepts a `recordId` parameter and fetches the record by baseId, tableId, and recordId without verifying that the caller owns or has access to that record. Add an ownership/permission check before returning the record: query Airtable's sharing/permissions API or the record's access control metadata to confirm the authenticated caller has read access to the record in the specified base and table. This prevents unauthorized users from reading records they do not own. Verify by attempting to fetch a record from a different user's base and confirming the request is rejected.
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 | { |
RemediationAI
The problem is that the tool name `list_tables` shadows a reserved database tool name from the MCP corpus, which may cause conflicts with standardized tool discovery or client expectations. Rename the tool to a unique name such as `list_airtable_tables` or `airtable_list_tables` in `manifest.json` and update all corresponding tool registration calls in the source code. This avoids namespace collisions and ensures the tool is discoverable without ambiguity. Verify by running the MCP server and confirming the tool appears with the new name in the tools list and can be invoked without conflicts.
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 | { |
RemediationAI
The problem is that the tool name `describe_table` shadows a reserved database tool name from the MCP corpus, which may cause conflicts with standardized tool discovery or client expectations. Rename the tool to a unique name such as `describe_airtable_table` or `airtable_describe_table` in `manifest.json` and update all corresponding tool registration calls in the source code. This avoids namespace collisions and ensures the tool is discoverable without ambiguity. Verify by running the MCP server and confirming the tool appears with the new name in the tools list and can be invoked without conflicts.
main.ts creates a single AirtableService instance with cached API key from process.env, then reuses it across all MCP tool invocations without per-caller authentication.
Evidence
| 15 | await cleanup(); |
| 16 | process.exit(0); |
| 17 | }); |
| 18 | } |
| 19 | |
| 20 | (async () => { |
| 21 | const apiKey = process.argv.slice(2)[0]; |
| 22 | if (apiKey) { |
| 23 | console.warn('warning (airtable-mcp-server): Passing in an API key as a command-line argument is deprecated and may be removed in a future version. Instead, set the `AIRTABLE_API_KEY` environment variable. See https://github.com/domdomegg/airtable-mcp-server/blob/master/README.md#usage for an example with Claude Desktop.'); |
RemediationAI
The problem is that a single `AirtableService` instance is created once at startup with a cached API key from `process.env`, then reused across all MCP tool invocations without per-caller authentication or credential isolation. Modify the architecture to create a new `AirtableService` instance per MCP request or caller, passing the caller's credentials (or a request-scoped token) to the constructor instead of reading from `process.env`. Alternatively, add a `setCredentials()` or `withCredentials()` method to `AirtableService` that accepts per-request credentials and uses them for that invocation only. This ensures each caller's API key is isolated and cannot leak to other callers. Verify by running two concurrent MCP requests with different API keys and confirming each uses only its own credentials.
AirtableService constructor reads AIRTABLE_API_KEY from process.env at initialization time and caches it as instance variable, then uses it for all Airtable API calls without consulting caller identity or per-request credentials.
Evidence
| 15 | FieldSchema, |
| 16 | CommentSchema, |
| 17 | ListCommentsResponseSchema, |
| 18 | AirtableRecordSchema, |
| 19 | type FieldSet, |
| 20 | } from './types.js'; |
| 21 | import {enhanceAirtableError} from './enhanceAirtableError.js'; |
| 22 | |
| 23 | export class AirtableService implements IAirtableService { |
| 24 | private readonly apiKey: string; |
| 25 | |
| 26 | private readonly baseUrl: string; |
| 27 | |
| 28 | private readonly fetch: typeof fetch; |
| 29 | |
| 30 | constructor( |
| 31 | apiKey: string = process.env.AIRTABLE_API_KEY || '', |
RemediationAI
The problem is that `AirtableService` reads `AIRTABLE_API_KEY` from `process.env` at initialization and caches it as an instance variable, then uses it for all Airtable API calls without consulting the caller's identity or accepting per-request credentials. Modify the constructor to accept an optional `apiKey` parameter instead of reading from `process.env`, and add a method like `withApiKey(key)` or `setApiKey(key)` to allow per-request credential injection. Update all API call methods to use the injected key instead of the cached instance variable. This enables per-caller authentication and prevents credential leakage. Verify by instantiating `AirtableService` with different API keys and confirming each instance uses only its own key.
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 |
RemediationAI
The problem is that the HTTP route handling MCP `tools/list` (and potentially `tools/call`) has no authentication middleware, allowing anonymous clients to enumerate all exposed tools and plan attacks. Add authentication middleware to the Express router before the MCP route handlers: use `passport.authenticate('bearer')` or a custom `requireAuth()` middleware that validates a JWT, API key, or session token. Apply this middleware at the router level so all routes are protected. This prevents unauthenticated enumeration and invocation of tools. Verify by making an unauthenticated request to the `tools/list` endpoint and confirming it returns a 401 or 403 error.
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 |
RemediationAI
The problem is that the Dockerfile does not set a non-root `USER` directive, so the container runs as root by default, giving any RCE or library vulnerability full system privileges. Add `USER 1000` (or `USER nobody` or `USER nonroot` on distroless) before the `CMD` or `ENTRYPOINT` instruction in the final stage of the Dockerfile. This ensures the MCP server process runs with minimal privileges. Verify by building the image, running the container, and confirming the process runs as a non-root user (e.g., `docker run ... id` should show uid != 0).
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 |
RemediationAI
The problem is that the MCP manifest declares tools but does not declare an authentication mechanism (no `auth`, `authorization`, `bearer`, `oauth`, `mtls`, `apiKey`, `api_key`, `basic`, `token`, or `authToken` field), making it unclear how the server is secured. Add an explicit `authentication` or `auth` field to the manifest (or README) documenting the real authentication mechanismโe.g., `"auth": "bearer"` for JWT tokens, `"auth": "api_key"` for API key validation, or `"auth": "network-layer"` if relying on network isolation. This clarifies the security model for reviewers and users. Verify by checking that the manifest or README now explicitly declares the authentication method and that the implementation matches the declaration.
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 |
RemediationAI
The problem is that state-changing HTTP routes (POST/PUT/PATCH/DELETE) registered in Express have no CSRF protection middleware, allowing cross-site requests to modify data if the user has an active session with cookies. Add CSRF middleware to the Express app: install and use `csurf` or `csrf-csrf` middleware before the route handlers (e.g., `app.use(csrf())`) and include CSRF tokens in all state-changing requests. Alternatively, if the API uses bearer token authentication (no cookies), CSRF is not applicable, but document this explicitly. This prevents cross-site forgery attacks. Verify by attempting a cross-site POST request without a CSRF token and confirming it is rejected with a 403 error.
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 |
RemediationAI
The problem is that `uses: softprops/action-gh-release@v2` is pinned to a mutable tag, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v2` with a full 40-character commit SHA, e.g., `uses: softprops/action-gh-release@1c61e1f3676c2344d5202ca0887688492b266d68 # v2.0.2`. This ensures the exact version is immutable and cannot be silently replaced. Verify by checking the workflow file and confirming all `uses:` references are pinned to commit SHAs.
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 }} |
RemediationAI
The problem is that `uses: docker/setup-buildx-action@v3` and `uses: docker/login-action@v3` are pinned to mutable tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v3` with full 40-character commit SHAs, e.g., `uses: docker/setup-buildx-action@0d103c3126aa41d772a8362f87519d63537ebf33 # v3.0.0` and `uses: docker/login-action@9780b0c1be7ead7b09a1ea8b2b817e8b34f6f1d7 # v3.0.0`. This ensures the exact versions are immutable. Verify by checking the workflow file and confirming all Docker action `uses:` references are pinned to commit SHAs.
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 |
RemediationAI
The problem is that `uses: docker/build-push-action@v5` is pinned to a mutable tag, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v5` with a full 40-character commit SHA, e.g., `uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f97d68 # v5.0.0`. This ensures the exact version is immutable and cannot be silently replaced. Verify by checking the workflow file and confirming the `uses:` reference is pinned to a commit SHA.
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: | |
RemediationAI
The problem is that `uses: docker/metadata-action@v5` is pinned to a mutable tag, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v5` with a full 40-character commit SHA, e.g., `uses: docker/metadata-action@8e5230c8d0d4c3c2d5e5c5c5c5c5c5c5c5c5c5c # v5.0.0`. This ensures the exact version is immutable and cannot be silently replaced. Verify by checking the workflow file and confirming the `uses:` reference is pinned to a commit SHA.
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: |
RemediationAI
The problem is that `uses: docker/setup-buildx-action@v3` and `uses: docker/login-action@v3` are pinned to mutable tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v3` with full 40-character commit SHAs, e.g., `uses: docker/setup-buildx-action@0d103c3126aa41d772a8362f87519d63537ebf33 # v3.0.0` and `uses: docker/login-action@9780b0c1be7ead7b09a1ea8b2b817e8b34f6f1d7 # v3.0.0`. This ensures the exact versions are immutable. Verify by checking the workflow file and confirming all Docker action `uses:` references are pinned to commit SHAs.
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/ |
RemediationAI
The problem is that `uses: actions/checkout@v4` and `uses: actions/setup-node@v4` are pinned to mutable tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v4` with full 40-character commit SHAs, e.g., `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.0.0` and `uses: actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf6ffb # v4.0.0`. This ensures the exact versions are immutable. Verify by checking the workflow file and confirming all GitHub action `uses:` references are pinned to commit SHAs.
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: |
RemediationAI
The problem is that `uses: actions/checkout@v4` and `uses: actions/setup-node@v4` are pinned to mutable tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v4` with full 40-character commit SHAs, e.g., `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.0.0` and `uses: actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf6ffb # v4.0.0`. This ensures the exact versions are immutable. Verify by checking the workflow file and confirming all GitHub action `uses:` references are pinned to commit SHAs.
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: | |
RemediationAI
The problem is that `uses: google-github-actions/auth@v2` and `uses: google-github-actions/setup-gcloud@v2` are pinned to mutable tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v2` with full 40-character commit SHAs, e.g., `uses: google-github-actions/auth@35793ba551a3b50efe432dd89f3e061215f08a33 # v2.0.0` and `uses: google-github-actions/setup-gcloud@62d4898025f6040ff5ce93f990b9cdca0541c48b # v2.0.0`. This ensures the exact versions are immutable. Verify by checking the workflow file and confirming all Google action `uses:` references are pinned to commit SHAs.
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/ |
RemediationAI
The problem is that `uses: actions/checkout@v4` and `uses: actions/setup-node@v4` are pinned to mutable tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v4` with full 40-character commit SHAs, e.g., `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.0.0` and `uses: actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf6ffb # v4.0.0`. This ensures the exact versions are immutable. Verify by checking the workflow file and confirming all GitHub action `uses:` references are pinned to commit SHAs.
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: |
RemediationAI
The problem is that `uses: actions/checkout@v4` and `uses: actions/setup-node@v4` are pinned to mutable tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v4` with full 40-character commit SHAs, e.g., `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.0.0` and `uses: actions/setup-node@60edb5dd545a775178fbb8726d51985e50cf6ffb # v4.0.0`. This ensures the exact versions are immutable. Verify by checking the workflow file and confirming all GitHub action `uses:` references are pinned to commit SHAs.
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/* |
RemediationAI
The problem is that `uses: actions/upload-artifact@v4` is pinned to a mutable tag, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v4` with a full 40-character commit SHA, e.g., `uses: actions/upload-artifact@26f46debb23297689e8f6b3b4b4bda3591e8c5d0 # v4.0.0`. This ensures the exact version is immutable and cannot be silently replaced. Verify by checking the workflow file and confirming the `uses:` reference is pinned to a commit SHA.
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 |
RemediationAI
The problem is that `uses: google-github-actions/auth@v2` is pinned to a mutable tag, allowing a compromised maintainer to inject malicious code into the CI pipeline. Replace `@v2` with a full 40-character commit SHA, e.g., `uses: google-github-actions/auth@35793ba551a3b50efe432dd89f3e061215f08a33 # v2.0.0`. This ensures the exact version is immutable and cannot be silently replaced. Verify by checking the workflow file and confirming the `uses:` reference is pinned to a commit SHA.