Mostly safe โ a couple of notes worth reading.
Scanned 5/3/2026, 7:26:23 PMยทCached resultยทFast Scanยท45 rulesยทHow we decide โ
AIVSS Score
Low
Severity Breakdown
0
critical
4
high
34
medium
7
low
MCP Server Information
Findings
This package carries moderate risk with a B grade and 68/100 safety score, driven primarily by 4 high-severity issues and 34 medium-severity findings concentrated in server configuration (24 findings) and verbose error handling (11 findings). The XXE vulnerabilities and readiness gaps present exploitable weaknesses that could enable information disclosure or denial of service attacks. You should address the high-severity findings and server configuration issues before deploying this to production.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 45 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.
Showing 1โ30 of 45 findings
45 findings
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 368 | for member in targets: |
| 369 | try: |
| 370 | xml_content = zf.read(member) |
| 371 | xml_root = ET.fromstring(xml_content) |
| 372 | member_texts: List[str] = [] |
| 373 | |
| 374 | if ( |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 338 | # Attempt to parse sharedStrings.xml for Excel files |
| 339 | try: |
| 340 | shared_strings_xml = zf.read("xl/sharedStrings.xml") |
| 341 | shared_strings_root = ET.fromstring(shared_strings_xml) |
| 342 | for si_element in shared_strings_root.findall( |
| 343 | f"{{{ns_excel_main}}}si" |
| 344 | ): |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
JWT signature verification explicitly disabled. JS: `jwt.verify(token, key, { algorithms: ["none"] })` โ accepts unsigned tokens. Any client can forge. Python PyJWT: `jwt.decode(token, verify=False)` or `options={"verify_signature": False}` โ same effect. A token whose signature is not verified is base64-encoded attacker input. Pass an explicit `algorithms` allow-list and keep verification on.
Evidence
| 1295 | if credentials and credentials.id_token: |
| 1296 | try: |
| 1297 | # Decode without verification (just to get email for logging) |
| 1298 | decoded_token = jwt.decode( |
| 1299 | credentials.id_token, options={"verify_signature": False} |
| 1300 | ) |
| 1301 | token_email = decoded_token.get("email") |
| 1302 | if token_email: |
Remediation
JS: `jwt.verify(token, key, { algorithms: ["RS256"] })` and reject on `JsonWebTokenError`. Never accept the `none` algorithm in the allow-list. Python PyJWT: default `verify=True`, pass an explicit `algorithms=["RS256"]` allow-list. Don't override `options={"verify_signature": False}`. After verification, only trust fields from the verified payload โ not from a separate `jwt.decode` call on the same token.
JWT signature verification explicitly disabled. JS: `jwt.verify(token, key, { algorithms: ["none"] })` โ accepts unsigned tokens. Any client can forge. Python PyJWT: `jwt.decode(token, verify=False)` or `options={"verify_signature": False}` โ same effect. A token whose signature is not verified is base64-encoded attacker input. Pass an explicit `algorithms` allow-list and keep verification on.
Evidence
| 139 | user_email = None |
| 140 | if credentials and credentials.id_token: |
| 141 | try: |
| 142 | decoded_token = jwt.decode( |
| 143 | credentials.id_token, options={"verify_signature": False} |
| 144 | ) |
| 145 | user_email = decoded_token.get("email") |
| 146 | except Exception as e: |
Remediation
JS: `jwt.verify(token, key, { algorithms: ["RS256"] })` and reject on `JsonWebTokenError`. Never accept the `none` algorithm in the allow-list. Python PyJWT: default `verify=True`, pass an explicit `algorithms=["RS256"]` allow-list. Don't override `options={"verify_signature": False}`. After verification, only trust fields from the verified payload โ not from a separate `jwt.decode` call on the same token.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 145 | except Exception as e: |
| 146 | logger.error(f"Failed to update {section_type}: {str(e)}") |
| 147 | return False, f"Failed to update {section_type}: {str(e)}" |
| 148 | |
| 149 | async def _get_document(self, document_id: str) -> dict[str, Any]: |
| 150 | """Get the full document data.""" |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 960 | image_uri = f"https://drive.google.com/uc?id={image_source}" |
| 961 | source_description = f"Drive file {file_metadata.get('name', image_source)}" |
| 962 | except Exception as e: |
| 963 | return f"Error: Could not access Drive file {image_source}: {str(e)}" |
| 964 | else: |
| 965 | image_uri = image_source |
| 966 | source_description = "URL image" |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 439 | except Exception as e: |
| 440 | logger.error(f"Failed to get header/footer info: {str(e)}") |
| 441 | return {"error": str(e)} |
| 442 | |
| 443 | def _extract_section_info(self, section_data: dict[str, Any]) -> dict[str, Any]: |
| 444 | """Extract useful information from a header/footer section.""" |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 373 | ) |
| 374 | |
| 375 | except Exception as e: |
| 376 | return False, f"Failed to populate existing table: {str(e)}", {} |
| 377 | |
| 378 | async def _populate_existing_table_cells_batch( |
| 379 | self, |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 2040 | return f"Successfully exported '{original_name}' to PDF and saved to Drive as '{pdf_filename}' (ID: {pdf_file_id}, {pdf_size:,} bytes){folder_info}. PDF: {pdf_web_link} | Original: {web_view_link}" |
| 2041 | |
| 2042 | except Exception as e: |
| 2043 | return f"Error: Failed to upload PDF to Drive: {str(e)}. PDF was generated successfully ({pdf_size:,} bytes) but could not be saved to Drive." |
| 2044 | |
| 2045 | |
| 2046 | # ============================================================================== |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 419 | except Exception as e: |
| 420 | logger.error(f"[run_script_function] Execution error: {str(e)}") |
| 421 | return f"Execution failed\nFunction: {function_name}\nError: {str(e)}" |
| 422 | |
| 423 | |
| 424 | @server.tool( |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 109 | except Exception as e: |
| 110 | logger.error(f"Failed to create and populate table: {str(e)}") |
| 111 | return False, f"Table creation failed: {str(e)}", {} |
| 112 | |
| 113 | async def _create_empty_table( |
| 114 | self, |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 1959 | .execute |
| 1960 | ) |
| 1961 | except Exception as e: |
| 1962 | return f"Error: Could not access document {document_id}: {str(e)}" |
| 1963 | |
| 1964 | mime_type = file_metadata.get("mimeType", "") |
| 1965 | original_name = file_metadata.get("name", "Unknown Document") |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 103 | except Exception as e: |
| 104 | error_message_detail = f"Error processing OAuth callback: {str(e)}" |
| 105 | logger.error(error_message_detail, exc_info=True) |
| 106 | return create_server_error_response(str(e)) |
| 107 | |
| 108 | def _setup_attachment_route(self): |
| 109 | """Setup the attachment serving route.""" |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 663 | return create_success_response(verified_user_id) |
| 664 | except Exception as e: |
| 665 | logger.error(f"Error processing OAuth callback: {str(e)}", exc_info=True) |
| 666 | return create_server_error_response(str(e)) |
| 667 | |
| 668 | |
| 669 | @server.tool( |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 1988 | pdf_size = len(pdf_content) |
| 1989 | |
| 1990 | except Exception as e: |
| 1991 | return f"Error: Failed to export document to PDF: {str(e)}" |
| 1992 | |
| 1993 | # Determine PDF filename |
| 1994 | if not pdf_filename: |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 277 | | file_name | string | yes | | Name for the new Google Doc | |
| 278 | | content | any | no | | Text content for MD, TXT, HTML | |
| 279 | | file_path | any | no | | Local file path for DOCX, ODT, etc. Supports `file://` URLs | |
| 280 | | file_url | any | no | | Remote URL to fetch (http/https) | |
| 281 | | source_format | any | no | (auto-detect) | `md`, `markdown`, `docx`, `txt`, `html`, `rtf`, `odt` | |
| 282 | | folder_id | string | no | root | Parent folder ID | |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
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
| 35 | """Result of saving an attachment: provides both the UUID and the absolute file path.""" |
| 36 | |
| 37 | file_id: str |
| 38 | path: str |
| 39 | |
| 40 | |
| 41 | class AttachmentStorage: |
| 42 | """Manages temporary storage of email attachments.""" |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
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
| 45 | needs: [autofix] |
| 46 | runs-on: ubuntu-latest |
| 47 | steps: |
| 48 | - uses: actions/checkout@v6 |
| 49 | - uses: actions/setup-python@v6 |
| 50 | with: |
| 51 | python-version: '3.11' |
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
| 88 | twine check dist/* |
| 89 | |
| 90 | - name: Publish package to PyPI |
| 91 | uses: pypa/gh-action-pypi-publish@release/v1 |
| 92 | with: |
| 93 | skip-existing: 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
| 16 | with: |
| 17 | python-version: '3.11' |
| 18 | - name: Install uv |
| 19 | uses: astral-sh/setup-uv@v7 |
| 20 | - name: Install dependencies |
| 21 | run: uv sync --extra test --frozen |
| 22 | - name: Run pytest |
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
| 27 | steps: |
| 28 | - name: Checkout repository |
| 29 | uses: actions/checkout@v6 |
| 30 | |
| 31 | - name: Set up Docker Buildx |
| 32 | uses: docker/setup-buildx-action@v3 |
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
| 46 | runs-on: ubuntu-latest |
| 47 | steps: |
| 48 | - uses: actions/checkout@v6 |
| 49 | - uses: actions/setup-python@v6 |
| 50 | with: |
| 51 | python-version: '3.11' |
| 52 | - name: Install uv |
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
| 16 | permissions: |
| 17 | contents: write |
| 18 | steps: |
| 19 | - uses: actions/checkout@v6 |
| 20 | with: |
| 21 | ref: ${{ github.event.pull_request.head.ref }} |
| 22 | token: ${{ secrets.GITHUB_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
| 12 | steps: |
| 13 | - uses: actions/checkout@v6 |
| 14 | - uses: actions/setup-python@v6 |
| 15 | with: |
| 16 | python-version: '3.11' |
| 17 | - name: Install uv |
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
| 50 | with: |
| 51 | python-version: '3.11' |
| 52 | - name: Install uv |
| 53 | uses: astral-sh/setup-uv@v7 |
| 54 | - name: Validate |
| 55 | # Use `uvx` so we run ruff in an isolated environment without invoking |
| 56 | # the project's build backend or executing pyproject.toml hooks. This |
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
| 42 | - name: Extract metadata (tags, labels) for Docker |
| 43 | id: meta |
| 44 | uses: docker/metadata-action@v5 |
| 45 | with: |
| 46 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} |
| 47 | 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
| 11 | runs-on: ubuntu-latest |
| 12 | |
| 13 | steps: |
| 14 | - uses: actions/checkout@v6 |
| 15 | - uses: actions/setup-python@v6 |
| 16 | with: |
| 17 | python-version: '3.11' |
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
| 15 | steps: |
| 16 | - name: Check if maintainer edits are enabled |
| 17 | uses: actions/github-script@v7 |
| 18 | with: |
| 19 | script: | |
| 20 | const { data: pr } = await github.rest.pulls.get({ |
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
| 19 | uses: actions/checkout@v6 |
| 20 | |
| 21 | - name: Set up Python |
| 22 | uses: actions/setup-python@v6 |
| 23 | with: |
| 24 | python-version: "3.11" |
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
| 16 | id-token: write |
| 17 | steps: |
| 18 | - name: Checkout |
| 19 | uses: actions/checkout@v6 |
| 20 | |
| 21 | - name: Set up Python |
| 22 | uses: actions/setup-python@v6 |
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 | with: |
| 21 | ref: ${{ github.event.pull_request.head.ref }} |
| 22 | token: ${{ secrets.GITHUB_TOKEN }} |
| 23 | - uses: actions/setup-python@v6 |
| 24 | with: |
| 25 | python-version: '3.11' |
| 26 | - name: Install uv |
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
| 30 | uses: actions/checkout@v6 |
| 31 | |
| 32 | - name: Set up Docker Buildx |
| 33 | uses: docker/setup-buildx-action@v3 |
| 34 | |
| 35 | - name: Log in to GitHub Container Registry |
| 36 | if: github.event_name != 'pull_request' |
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
| 55 | type=raw,value=latest,enable={{is_default_branch}} |
| 56 | |
| 57 | - name: Build and push Docker image |
| 58 | uses: docker/build-push-action@v6 |
| 59 | with: |
| 60 | context: . |
| 61 | push: ${{ github.event_name != 'pull_request' }} |
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
| 24 | with: |
| 25 | python-version: '3.11' |
| 26 | - name: Install uv |
| 27 | uses: astral-sh/setup-uv@v7 |
| 28 | - name: Auto-fix ruff lint and format |
| 29 | # Same-repo only (guarded by job `if`), so we still avoid `uv sync` |
| 30 | # to keep behaviour consistent and minimise blast radius. |
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
| 34 | - name: Log in to GitHub Container Registry |
| 35 | if: github.event_name != 'pull_request' |
| 36 | uses: docker/login-action@v3 |
| 37 | with: |
| 38 | registry: ${{ env.REGISTRY }} |
| 39 | username: ${{ github.actor }} |
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
MCP server uses `session_id` as the sole key for session-data lookup without binding it to a user identifier. The official MCP security best practices require keys to combine user_id + session_id (e.g. `<user_id>:<session_id>`) so a guessed/obtained session_id alone cannot impersonate another user. Fix: use a compound key in DDB (`Key={"user_id": user_id, "session_id": session_id}`), or build the key as `f"{user_id}:{session_id}"` for dict/cache/redis access. Sourced `user_id` MUST come from th
Evidence
| 771 | return self.get_credentials(requested_user_email) |
| 772 | |
| 773 | # Check if this is an MCP session |
| 774 | mcp_user = self._mcp_session_mapping.get(session_id) |
| 775 | if mcp_user: |
| 776 | if mcp_user != requested_user_email: |
| 777 | logger.error( |
Remediation
Bind session IDs to user IDs in every key: # BAD ddb.get_item(Key={"session_id": session_id}) data = sessions[session_id] redis.get(session_id) # GOOD ddb.get_item(Key={"user_id": user_id, "session_id": session_id}) data = sessions[f"{user_id}:{session_id}"] redis.get(f"{user_id}:{session_id}") The user_id should come from the verified authorization token (JWT claim, validated session cookie) โ never from a request parameter the user can choose.
MCP server uses `session_id` as the sole key for session-data lookup without binding it to a user identifier. The official MCP security best practices require keys to combine user_id + session_id (e.g. `<user_id>:<session_id>`) so a guessed/obtained session_id alone cannot impersonate another user. Fix: use a compound key in DDB (`Key={"user_id": user_id, "session_id": session_id}`), or build the key as `f"{user_id}:{session_id}"` for dict/cache/redis access. Sourced `user_id` MUST come from th
Evidence
| 40 | Set or clear the FastMCP session ID for the current request context. |
| 41 | This is called when a FastMCP request starts. |
| 42 | """ |
| 43 | _fastmcp_session_id.set(session_id) |
Remediation
Bind session IDs to user IDs in every key: # BAD ddb.get_item(Key={"session_id": session_id}) data = sessions[session_id] redis.get(session_id) # GOOD ddb.get_item(Key={"user_id": user_id, "session_id": session_id}) data = sessions[f"{user_id}:{session_id}"] redis.get(f"{user_id}:{session_id}") The user_id should come from the verified authorization token (JWT claim, validated session cookie) โ never from a request parameter the user can choose.
MCP server uses `session_id` as the sole key for session-data lookup without binding it to a user identifier. The official MCP security best practices require keys to combine user_id + session_id (e.g. `<user_id>:<session_id>`) so a guessed/obtained session_id alone cannot impersonate another user. Fix: use a compound key in DDB (`Key={"user_id": user_id, "session_id": session_id}`), or build the key as `f"{user_id}:{session_id}"` for dict/cache/redis access. Sourced `user_id` MUST come from th
Evidence
| 759 | # Priority 2: Check session binding |
| 760 | if session_id: |
| 761 | bound_user = self._session_auth_binding.get(session_id) |
| 762 | if bound_user: |
| 763 | if bound_user != requested_user_email: |
| 764 | logger.error( |
Remediation
Bind session IDs to user IDs in every key: # BAD ddb.get_item(Key={"session_id": session_id}) data = sessions[session_id] redis.get(session_id) # GOOD ddb.get_item(Key={"user_id": user_id, "session_id": session_id}) data = sessions[f"{user_id}:{session_id}"] redis.get(f"{user_id}:{session_id}") The user_id should come from the verified authorization token (JWT claim, validated session cookie) โ never from a request parameter the user can choose.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 347 | store = get_oauth21_session_store() |
| 348 | if store.has_session(authenticated_user): |
| 349 | return "oauth21" |
| 350 | except (ImportError, AttributeError, RuntimeError): |
| 351 | pass # Fall back to OAuth 2.0 if session check fails |
| 352 | |
| 353 | # For public clients in OAuth 2.1 mode, we require PKCE |
| 354 | # But since they didn't send PKCE, fall back to OAuth 2.0 |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 263 | logger.info( |
| 264 | f"Minimal OAuth server started on {hostname}:{self.port}" |
| 265 | ) |
| 266 | return True, "" |
| 267 | except Exception: |
| 268 | pass |
| 269 | time.sleep(0.1) |
| 270 | |
| 271 | error_msg = f"Failed to start minimal OAuth server on {hostname}:{self.port} - server did not respond within {max_wait}s" |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 385 | doc = await self._get_document(document_id) |
| 386 | return self._resolve_section_id_from_styles( |
| 387 | doc, section_type, header_footer_type |
| 388 | ) |
| 389 | except Exception: |
| 390 | pass |
| 391 | logger.error(f"Failed to create missing {section_type}: {str(e)}") |
| 392 | return None |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 926 | if credentials.valid: |
| 927 | return credentials |
| 928 | |
| 929 | return None |
| 930 | except ImportError: |
| 931 | pass # OAuth 2.1 store not available |
| 932 | except Exception as e: |
| 933 | logger.debug(f"[get_credentials] Error checking OAuth 2.1 store: {e}") |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 197 | # Update in args if user_google_email is passed positionally |
| 198 | try: |
| 199 | user_email_index = param_names.index("user_google_email") |
| 200 | args = _update_email_in_args(args, user_email_index, authenticated_user) |
| 201 | except ValueError: |
| 202 | pass # user_google_email not in positional parameters |
| 203 | |
| 204 | return authenticated_user, args |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 308 | "Generated scopes from granular permissions: %d unique scopes", |
| 309 | len(set(scopes)), |
| 310 | ) |
| 311 | return list(set(scopes)) |
| 312 | except ImportError: |
| 313 | pass |
| 314 | |
| 315 | if enabled_tools is None: |
| 316 | # Default behavior - return all scopes |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 288 | # Clean up if we created the directory but can't write to it |
| 289 | try: |
| 290 | if os.path.exists(credentials_dir): |
| 291 | os.rmdir(credentials_dir) |
| 292 | except (PermissionError, OSError): |
| 293 | pass |
| 294 | raise PermissionError( |
| 295 | f"Cannot create or write to credentials directory '{os.path.abspath(credentials_dir)}': {e}" |
| 296 | ) |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.