Use with caution. Address findings before production.
Scanned 5/12/2026, 7:16:26 PMยทCached resultยทDeep Scanยท91 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
3
high
25
medium
0
low
MCP Server Information
Findings
This package has a concerning security profile with a C grade and a safety score of 62/100, driven by 28 findingsโthree high-severity and 25 medium-severity issues. The risks stem mostly from server configuration flaws (24 findings) and a smaller number of prompt injection and insecure container image vulnerabilities, though no critical or top-priority threats were detected. Proceed with caution, especially if your environment relies on secure server setups or handles untrusted input.
AIPer-finding remediation generated by bedrock-claude-haiku-4-5 โ 28 of 28 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.
28 of 28 findings
28 findings
Firecrawl MCP tools use FIRECRAWL_API_KEY from environment variables (dotenv.config) without consulting per-request caller identity, creating a confused deputy vulnerability when calling Firecrawl API.
Evidence
| 1 | #!/usr/bin/env node |
| 2 | import dotenv from 'dotenv'; |
| 3 | import { FastMCP, type Logger } from 'firecrawl-fastmcp'; |
| 4 | import { z } from 'zod'; |
| 5 | import FirecrawlApp from '@mendable/firecrawl-js'; |
| 6 | import type { IncomingHttpHeaders } from 'http'; |
| 7 | import { readFile } from 'node:fs/promises'; |
| 8 | import path from 'node:path'; |
| 9 | |
| 10 | dotenv.config({ debug: false, quiet: true }); |
| 11 | |
| 12 | interface SessionData { |
| 13 | firecrawlApiKey?: string; |
| 14 | [key: string]: unknown; |
| 15 | } |
| 16 | |
| 17 | function extractApiKey(headers: IncomingHttpHeaders): string | u |
RemediationAI
The problem is that `dotenv.config()` loads FIRECRAWL_API_KEY into a global environment variable without validating which caller (user/tenant) is making the request, allowing any caller to use the same API key for their own requests. Replace the global `dotenv.config()` approach with per-request API key extraction from request headers or context: modify the tool handlers to accept an optional `apiKey` parameter from the MCP request context and pass it explicitly to `new FirecrawlApp({ apiKey })` instead of relying on the environment variable. This ensures each caller's credentials are isolated and cannot be confused with another caller's identity. Verify by adding a test that creates two separate MCP requests with different `X-API-Key` headers and confirming each uses its own key, not the global one.
Firecrawl MCP server fetches untrusted web content via search() and scraping tools, returning markdown/HTML from arbitrary URLs supplied by callers without provenance delimiters, enabling indirect prompt injection into the LLM context.
Evidence
| 1 | #!/usr/bin/env node |
| 2 | import dotenv from 'dotenv'; |
| 3 | import { FastMCP, type Logger } from 'firecrawl-fastmcp'; |
| 4 | import { z } from 'zod'; |
RemediationAI
The problem is that `search()` and scraping tools return raw markdown/HTML from arbitrary URLs without any delimiters or provenance markers, allowing an attacker to inject malicious content (e.g., fake tool definitions or instructions) that the LLM will treat as trusted context. Wrap all returned content with explicit provenance delimiters and metadata: modify the tool handlers to return `{ source: url, timestamp, content: "<UNTRUSTED_CONTENT>\n" + markdown + "\n</UNTRUSTED_CONTENT>" }` and document in the tool description that content is user-supplied and should not be trusted for code execution or sensitive decisions. This makes the LLM aware that the content is external and untrusted. Verify by returning a URL with injected prompt instructions and confirming the LLM does not execute them as commands.
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 { Server } from '@modelcontextprotocol/sdk/server/index.js'; |
| 4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; |
| 5 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; |
| 6 | import { |
| 7 | Tool, |
| 8 | CallToolRequestSchema, |
| 9 | ListToolsRequestSchema, |
| 10 | } from '@modelcontextprotocol/sdk/types.js'; |
| 11 | import FirecrawlApp, { |
| 12 | type ScrapeOptions, |
| 13 | type MapOptions, |
| 14 | type Document, |
| 15 | } from '@mendable/firecrawl-js'; |
| 16 | |
| 17 | import { StreamableHTTP |
RemediationAI
The problem is that the `tools/list` route (and likely `tools/call`) has no authentication middleware, allowing anonymous clients to enumerate all available tools and invoke them without authorization. Add authentication middleware at the router level before the route handler: for Express, wrap the route with `app.use(passport.authenticate('bearer', { session: false }))` or a custom `requireAuth` middleware; for FastAPI, add `Depends(verify_jwt)` to the route function signature; for Flask, decorate with `@require_auth` before the route handler. This ensures only authenticated callers can list or invoke tools. Verify by making an unauthenticated HTTP request to `GET /tools/list` and confirming it returns 401 Unauthorized, then with a valid token confirming it returns 200.
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 348 | text: z.string().optional(), |
| 349 | key: z.string().optional(), |
| 350 | direction: z.enum(['up', 'down']).optional(), |
| 351 | script: z.string().optional(), |
| 352 | fullPage: z.boolean().optional(), |
| 353 | }) |
| 354 | ) |
RemediationAI
The problem is that the `script` field accepts an unconstrained string, allowing callers to pass arbitrary JavaScript/shell code that may be executed or logged unsafely. Constrain the `script` field with a maximum length and optional regex pattern: change `script: z.string().optional()` to `script: z.string().max(5000).regex(/^[a-zA-Z0-9_\-\s.(){}[\]]*$/).optional()` to allow only safe characters, or use `.enum()` if only specific scripts are valid. This prevents injection of malicious code through the script parameter. Verify by attempting to pass a script with shell metacharacters (e.g., `; rm -rf /`) and confirming the schema validation rejects it.
LLM consensus
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 650 | `, |
| 651 | parameters: z |
| 652 | .object({ |
| 653 | query: z.string().min(1), |
| 654 | limit: z.number().optional(), |
| 655 | tbs: z.string().optional(), |
| 656 | filter: z.string().optional(), |
RemediationAI
The problem is that the `query` field accepts an unconstrained string with `.min(1)` but no upper bound, allowing callers to pass extremely long or malicious search queries that could cause DoS or injection attacks. Add a maximum length constraint: change `query: z.string().min(1)` to `query: z.string().min(1).max(500)` to limit query size, and optionally add `.regex(/^[a-zA-Z0-9\s\-_.]*$/)` to restrict to safe search characters. This prevents abuse through oversized or specially-crafted queries. Verify by passing a 10,000-character query string and confirming the schema validation rejects it.
LLM consensus
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 1311 | parameters: z.object({ |
| 1312 | scrapeId: z.string(), |
| 1313 | prompt: z.string().optional(), |
| 1314 | code: z.string().optional(), |
| 1315 | language: z.enum(['bash', 'python', 'node']).optional(), |
| 1316 | timeout: z.number().min(1).max(300).optional(), |
| 1317 | }).refine(data => data.code || data.prompt, { |
RemediationAI
The problem is that the `code` field accepts an unconstrained string, allowing callers to pass arbitrary code of unlimited length that will be executed in the specified language (bash, python, node). Constrain the `code` field with a maximum length: change `code: z.string().optional()` to `code: z.string().max(10000).optional()` to limit code size and prevent DoS or excessively complex payloads. This prevents attackers from submitting infinite loops or memory-exhausting code. Verify by passing a code string larger than 10,000 characters and confirming the schema validation rejects it.
LLM consensus
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 552 | **Returns:** Array of URLs found on the site, filtered by search query if provided. |
| 553 | `, |
| 554 | parameters: z.object({ |
| 555 | url: z.string().url(), |
| 556 | search: z.string().optional(), |
| 557 | sitemap: z.enum(['include', 'skip', 'only']).optional(), |
| 558 | includeSubdomains: z.boolean().optional(), |
RemediationAI
The problem is that the `search` field accepts an unconstrained string, allowing callers to pass arbitrary search terms that could be used for injection or DoS attacks against the URL-scraping backend. Constrain the `search` field with a maximum length: change `search: z.string().optional()` to `search: z.string().max(200).optional()` to limit search term size and prevent abuse. This reduces the attack surface for injection or resource exhaustion. Verify by passing a 5,000-character search string and confirming the schema validation rejects it.
LLM consensus
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 1165 | `, |
| 1166 | parameters: z.object({ |
| 1167 | sessionId: z.string(), |
| 1168 | code: z.string(), |
| 1169 | language: z.enum(['bash', 'python', 'node']).optional(), |
| 1170 | }), |
| 1171 | execute: async ( |
RemediationAI
The problem is that the `code` field accepts an unconstrained string, allowing callers to pass arbitrary code of unlimited length that will be executed in a session context. Constrain the `code` field with a maximum length: change `code: z.string()` to `code: z.string().max(10000)` to limit code size and prevent DoS or excessively complex payloads. This prevents attackers from submitting infinite loops or memory-exhausting code. Verify by passing a code string larger than 10,000 characters and confirming the schema validation rejects it.
LLM consensus
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 287 | } |
| 288 | |
| 289 | const scrapeParamsSchema = z.object({ |
| 290 | url: z.string().url(), |
| 291 | formats: z |
| 292 | .array( |
| 293 | z.enum([ |
RemediationAI
The problem is that the `url` field accepts an unconstrained string URL, allowing callers to pass URLs to internal services, private IPs, or malicious domains without validation. Constrain the `url` field with a whitelist or regex: change `url: z.string().url()` to `url: z.string().url().refine(u => !u.includes('localhost') && !u.includes('127.0.0.1') && !u.includes('192.168') && !u.includes('10.0'), { message: 'Private IPs not allowed' })` to block internal addresses, or use `.regex(/^https:\/\/[a-z0-9.-]+\.(com|org|net)$/)` to allow only specific domains. This prevents SSRF attacks. Verify by attempting to pass `http://localhost:8080` and confirming the schema validation rejects it.
LLM consensus
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 742 | } |
| 743 | `, |
| 744 | parameters: z.object({ |
| 745 | url: z.string(), |
| 746 | prompt: z.string().optional(), |
| 747 | excludePaths: z.array(z.string()).optional(), |
| 748 | includePaths: z.array(z.string()).optional(), |
RemediationAI
The problem is that the `prompt` field accepts an unconstrained string, allowing callers to pass extremely long or malicious prompts that could cause DoS or injection attacks. Constrain the `prompt` field with a maximum length: change `prompt: z.string().optional()` to `prompt: z.string().max(5000).optional()` to limit prompt size and prevent abuse. This prevents attackers from submitting oversized or specially-crafted prompts. Verify by passing a 50,000-character prompt string and confirming the schema validation rejects it.
LLM consensus
Package declares an install-time hook (npm postinstall/preinstall/prepare, setup.py cmdclass override, custom setuptools install class, or non-default pyproject build-backend). Anyone installing this package runs the hook. Confirm the hook is necessary and review its contents; prefer shipping a plain library without install-time execution.
Evidence
| 22 | "lint": "eslint src/**/*.ts", |
| 23 | "lint:fix": "eslint src/**/*.ts --fix", |
| 24 | "format": "prettier --write .", |
| 25 | "prepare": "npm run build", |
| 26 | "publish-prod": "npm run build && npm publish", |
| 27 | "publish-beta": "npm run build && npm publish --tag beta" |
| 28 | }, |
RemediationAI
The problem is that the `prepare` script runs `npm run build` at install time, executing arbitrary code whenever the package is installed, which could be exploited if the build script is compromised or if a dependency is hijacked. Remove or defer the install-time hook: change the `prepare` script to a manual build step by removing the `"prepare": "npm run build"` line from `package.json` and documenting that users must run `npm run build` manually after installation, or move the build to a CI/CD pipeline instead. This prevents automatic code execution during installation. Verify by running `npm install` in a fresh directory and confirming no build occurs automatically, then manually running `npm run build` and confirming it works.
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 as the base for building the application |
| 3 | FROM node:22-alpine AS builder |
| 4 | |
| 5 | # Enable pnpm via corepack |
| 6 | RUN corepack enable && corepack prepare pnpm@latest --activate |
| 7 | |
| 8 | # Set the working directory inside the container |
| 9 | WORKDIR /app |
| 10 | |
| 11 | # Copy package.json and pnpm-lock.yaml to install dependencies |
| 12 | COPY package.json pnpm-lock.yaml ./ |
| 13 | |
| 14 | # Install dependencies (ignoring scripts to prevent running the pre |
RemediationAI
The problem is that the Dockerfile runs the application as root, so any RCE vulnerability or malicious code execution inside the container gains full system privileges. Add a non-root user before the CMD: insert `RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser` and `USER 1000` before the final `CMD` or `ENTRYPOINT` directive in the Dockerfile. This limits the blast radius of any container escape or code execution to unprivileged user permissions. Verify by building the image, running it, and executing `id` inside the container to confirm the process runs as UID 1000 (non-root).
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 | ## Service image: Node (FastMCP) + NGINX sidecar in one container |
| 2 | |
| 3 | # 1) Build stage |
| 4 | FROM node:22-alpine AS builder |
| 5 | WORKDIR /app |
| 6 | |
| 7 | # Enable pnpm via corepack |
| 8 | RUN corepack enable && corepack prepare pnpm@latest --activate |
| 9 | |
| 10 | COPY package.json pnpm-lock.yaml ./ |
| 11 | # Install dev dependencies for the build, but skip scripts to avoid running |
| 12 | # the package "prepare" script before the source code is copied. |
| 13 | RUN pnpm install --frozen-lockfile --ignore-scripts |
| 14 | |
| 15 | COPY . . |
| 16 | RUN pnpm run build |
| 17 | |
| 18 | |
| 19 | # 2) Runtime stage ( |
RemediationAI
The problem is that the Dockerfile.service runs the application as root, so any RCE vulnerability or malicious code execution inside the container gains full system privileges. Add a non-root user before the CMD: insert `RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser` and `USER 1000` before the final `CMD` or `ENTRYPOINT` directive in the Dockerfile.service. This limits the blast radius of any container escape or code execution to unprivileged user permissions. Verify by building the image, running it, and executing `id` inside the container to confirm the process runs as UID 1000 (non-root).
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 | <div align="center"> |
| 2 | <a name="readme-top"></a> |
| 3 | <img |
| 4 | src="https://raw.githubusercontent.com/firecrawl/firecrawl-mcp-server/main/img/fire.png" |
| 5 | height="140" |
| 6 | > |
| 7 | </div> |
| 8 | |
| 9 | # Firecrawl MCP Server |
| 10 | |
| 11 | A Model Context Protocol (MCP) server implementation that integrates with [Firecrawl](https://github.com/firecrawl/firecrawl) for searching, scraping, and interacting with the web. |
| 12 | |
| 13 | > Big thanks to [@vrknetha](https://github.com/vrknetha), [@knacklabs](https://www.knacklabs.ai) for the initial imp |
RemediationAI
The problem is that the README and manifest do not declare any authentication mechanism, making it unclear whether the server is protected and how reviewers should audit access control. Add an explicit `auth` or `authorization` field to the manifest and document it in the README: add a section like `## Authentication: This server requires a valid Firecrawl API key passed via the FIRECRAWL_API_KEY environment variable or per-request header` and update the manifest JSON to include `"auth": { "type": "apiKey", "location": "header", "name": "X-API-Key" }`. This makes the authentication model explicit and auditable. Verify by reviewing the README and confirming the authentication mechanism is clearly documented for users and reviewers.
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 { Server } from '@modelcontextprotocol/sdk/server/index.js'; |
| 4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; |
| 5 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; |
| 6 | import { |
| 7 | Tool, |
| 8 | CallToolRequestSchema, |
| 9 | ListToolsRequestSchema, |
| 10 | } from '@modelcontextprotocol/sdk/types.js'; |
| 11 | import FirecrawlApp, { |
| 12 | type ScrapeOptions, |
| 13 | type MapOptions, |
| 14 | type Document, |
| 15 | } from '@mendable/firecrawl-js'; |
| 16 | |
| 17 | import { StreamableHTTP |
RemediationAI
The problem is that state-changing routes (POST/PUT/PATCH/DELETE) lack CSRF protection, allowing cross-site requests to modify state while the user's cookies ride along if cookie-based sessions are used. Apply CSRF middleware at the router level: for Express, add `const csrf = require('csurf'); app.use(csrf()); app.post('/route', (req, res) => { /* handler */ })` to protect all POST routes; for FastAPI, add `from fastapi_csrf_protect import CsrfProtect; CsrfProtect(app)` and decorate routes with `@csrf_protect`; for Flask, add `from flask_wtf.csrf import CSRFProtect; csrf = CSRFProtect(app)`. This prevents CSRF attacks by validating tokens on state-changing requests. Verify by making a cross-origin POST request without a CSRF token and confirming it is rejected with 403 Forbidden.
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
| 17 | steps: |
| 18 | |
| 19 | - name: 'Checkout GitHub Action' |
| 20 | uses: actions/checkout@main |
| 21 | |
| 22 | - name: 'Login to GitHub Container Registry' |
| 23 | uses: docker/login-action@v1 |
RemediationAI
The problem is that `actions/checkout@main` uses a mutable branch reference, allowing a compromised maintainer or tag rewrite to inject malicious code into the CI pipeline. Pin the action to a specific commit SHA: replace `uses: actions/checkout@main` with `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` (or the appropriate SHA for the desired version). This ensures the exact version of the action is used and cannot be silently replaced. Verify by running the workflow and confirming the action uses the pinned SHA in the logs.
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
| 24 | password: ${{secrets.GITHUB_TOKEN}} |
| 25 | |
| 26 | - name: 'Set up Docker Buildx' |
| 27 | uses: docker/setup-buildx-action@v1 |
| 28 | |
| 29 | - name: 'Build Service Image' |
| 30 | uses: docker/build-push-action@v2 |
RemediationAI
The problem is that `docker/login-action@v1` and `docker/build-push-action@v2` use mutable version tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Pin the actions to specific commit SHAs: replace `uses: docker/login-action@v1` with `uses: docker/login-action@9780b0c56667666bca7ee0d6ff1a1c5a7edc33d4 # v1.5.0` and `uses: docker/build-push-action@v2` with `uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v2.10.0` (or appropriate SHAs). This ensures the exact versions are used and cannot be silently replaced. Verify by running the workflow and confirming the actions use the pinned SHAs in the logs.
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
| 17 | contents: read |
| 18 | |
| 19 | steps: |
| 20 | - uses: actions/checkout@v5 |
| 21 | |
| 22 | - name: Install pnpm |
| 23 | uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 |
RemediationAI
The problem is that `actions/checkout@v5` and `actions/setup-node@v4` use mutable version tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Pin the actions to specific commit SHAs: replace `uses: actions/checkout@v5` with `uses: actions/checkout@a5ac7e51b41094c153674e0e265ab8620f0be3fb # v4.1.1` and `uses: actions/setup-node@v4` with `uses: actions/setup-node@60edb5dd545a775178fba8f164beabf835104efa # v4.0.2` (or appropriate SHAs). This ensures the exact versions are used and cannot be silently replaced. Verify by running the workflow and confirming the actions use the pinned SHAs in the logs.
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
| 25 | version: 10 |
| 26 | |
| 27 | - name: Set up Node.js |
| 28 | uses: actions/setup-node@v4 |
| 29 | with: |
| 30 | node-version: "20" |
| 31 | cache: "pnpm" |
RemediationAI
The problem is that `actions/setup-node@v4` uses a mutable version tag, allowing a compromised maintainer to inject malicious code into the CI pipeline. Pin the action to a specific commit SHA: replace `uses: actions/setup-node@v4` with `uses: actions/setup-node@60edb5dd545a775178fba8f164beabf835104efa # v4.0.2` (or the appropriate SHA for the desired version). This ensures the exact version of the action is used and cannot be silently replaced. Verify by running the workflow and confirming the action uses the pinned SHA in the logs.
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
| 11 | runs-on: ubuntu-latest |
| 12 | |
| 13 | steps: |
| 14 | - uses: actions/checkout@v3 |
| 15 | |
| 16 | - name: Setup pnpm |
| 17 | uses: pnpm/action-setup@v4 |
RemediationAI
The problem is that `actions/checkout@v3` and `pnpm/action-setup@v4` use mutable version tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Pin the actions to specific commit SHAs: replace `uses: actions/checkout@v3` with `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` and `uses: pnpm/action-setup@v4` with `uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.0.0` (or appropriate SHAs). This ensures the exact versions are used and cannot be silently replaced. Verify by running the workflow and confirming the actions use the pinned SHAs in the logs.
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
| 27 | uses: docker/setup-buildx-action@v1 |
| 28 | |
| 29 | - name: 'Build Service Image' |
| 30 | uses: docker/build-push-action@v2 |
| 31 | with: |
| 32 | context: . |
| 33 | file: ./Dockerfile.service |
RemediationAI
The problem is that `docker/setup-buildx-action@v1` and `docker/build-push-action@v2` use mutable version tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Pin the actions to specific commit SHAs: replace `uses: docker/setup-buildx-action@v1` with `uses: docker/setup-buildx-action@4c0219f9ac624f65b47eb3123aafdf1e458a5480 # v1.2.0` and `uses: docker/build-push-action@v2` with `uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v2.10.0` (or appropriate SHAs). This ensures the exact versions are used and cannot be silently replaced. Verify by running the workflow and confirming the actions use the pinned SHAs in the logs.
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
| 14 | - uses: actions/checkout@v3 |
| 15 | |
| 16 | - name: Setup pnpm |
| 17 | uses: pnpm/action-setup@v4 |
| 18 | with: |
| 19 | version: 10 |
RemediationAI
The problem is that `pnpm/action-setup@v4` uses a mutable version tag, allowing a compromised maintainer to inject malicious code into the CI pipeline. Pin the action to a specific commit SHA: replace `uses: pnpm/action-setup@v4` with `uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.0.0` (or the appropriate SHA for the desired version). This ensures the exact version of the action is used and cannot be silently replaced. Verify by running the workflow and confirming the action uses the pinned SHA in the logs.
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
| 27 | password: ${{ secrets.GITHUB_TOKEN }} |
| 28 | |
| 29 | - name: 'Set up Docker Buildx' |
| 30 | uses: docker/setup-buildx-action@v1 |
| 31 | |
| 32 | - name: 'Build Service Image' |
| 33 | uses: docker/build-push-action@v2 |
RemediationAI
The problem is that `docker/setup-buildx-action@v1` and `docker/build-push-action@v2` use mutable version tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Pin the actions to specific commit SHAs: replace `uses: docker/setup-buildx-action@v1` with `uses: docker/setup-buildx-action@4c0219f9ac624f65b47eb3123aafdf1e458a5480 # v1.2.0` and `uses: docker/build-push-action@v2` with `uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v2.10.0` (or appropriate SHAs). This ensures the exact versions are used and cannot be silently replaced. Verify by running the workflow and confirming the actions use the pinned SHAs in the logs.
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 | uses: actions/checkout@main |
| 21 | |
| 22 | - name: 'Login to GitHub Container Registry' |
| 23 | uses: docker/login-action@v1 |
| 24 | with: |
| 25 | registry: ghcr.io |
| 26 | username: ${{ github.actor }} |
RemediationAI
The problem is that `actions/checkout@main` uses a mutable branch reference, allowing a compromised maintainer or branch rewrite to inject malicious code into the CI pipeline. Pin the action to a specific commit SHA: replace `uses: actions/checkout@main` with `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` (or the appropriate SHA for the desired version). This ensures the exact version of the action is used and cannot be silently replaced. Verify by running the workflow and confirming the action uses the pinned SHA in the logs.
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
| 17 | uses: actions/checkout@main |
| 18 | |
| 19 | - name: 'Login to GitHub Container Registry' |
| 20 | uses: docker/login-action@v1 |
| 21 | with: |
| 22 | registry: ghcr.io |
| 23 | username: ${{github.actor}} |
RemediationAI
The problem is that `actions/checkout@main` uses a mutable branch reference, allowing a compromised maintainer or branch rewrite to inject malicious code into the CI pipeline. Pin the action to a specific commit SHA: replace `uses: actions/checkout@main` with `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` (or the appropriate SHA for the desired version). This ensures the exact version of the action is used and cannot be silently replaced. Verify by running the workflow and confirming the action uses the pinned SHA in the logs.
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
| 14 | runs-on: ubuntu-latest |
| 15 | steps: |
| 16 | - name: 'Checkout GitHub Action' |
| 17 | uses: actions/checkout@main |
| 18 | |
| 19 | - name: 'Login to GitHub Container Registry' |
| 20 | uses: docker/login-action@v1 |
RemediationAI
The problem is that `actions/checkout@main` uses a mutable branch reference, allowing a compromised maintainer or branch rewrite to inject malicious code into the CI pipeline. Pin the action to a specific commit SHA: replace `uses: actions/checkout@main` with `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` (or the appropriate SHA for the desired version). This ensures the exact version of the action is used and cannot be silently replaced. Verify by running the workflow and confirming the action uses the pinned SHA in the logs.
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
| 19 | version: 10 |
| 20 | |
| 21 | - name: Use Node.js |
| 22 | uses: actions/setup-node@v3 |
| 23 | with: |
| 24 | node-version: '20.x' |
| 25 | cache: 'pnpm' |
RemediationAI
The problem is that `actions/setup-node@v3` uses a mutable version tag, allowing a compromised maintainer to inject malicious code into the CI pipeline. Pin the action to a specific commit SHA: replace `uses: actions/setup-node@v3` with `uses: actions/setup-node@8f152de45cc393bb48ce5d6f9a02a8fd8a2c40ba # v3.5.1` (or the appropriate SHA for the desired version). This ensures the exact version of the action is used and cannot be silently replaced. Verify by running the workflow and confirming the action uses the pinned SHA in the logs.
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
| 30 | uses: docker/setup-buildx-action@v1 |
| 31 | |
| 32 | - name: 'Build Service Image' |
| 33 | uses: docker/build-push-action@v2 |
| 34 | with: |
| 35 | context: . |
| 36 | file: ./Dockerfile.service |
RemediationAI
The problem is that `docker/setup-buildx-action@v1` and `docker/build-push-action@v2` use mutable version tags, allowing a compromised maintainer to inject malicious code into the CI pipeline. Pin the actions to specific commit SHAs: replace `uses: docker/setup-buildx-action@v1` with `uses: docker/setup-buildx-action@4c0219f9ac624f65b47eb3123aafdf1e458a5480 # v1.2.0` and `uses: docker/build-push-action@v2` with `uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v2.10.0` (or appropriate SHAs). This ensures the exact versions are used and cannot be silently replaced. Verify by running the workflow and confirming the actions use the pinned SHAs in the logs.
readme-top