Back to skill

Security audit

FastAPI Production Engineering

Security checks for vulnerabilities and agentic risk

Overview

This FastAPI guidance skill is mostly transparent, but some production templates teach unsafe upload and WebSocket patterns and use an unpinned build tool install.

Use this skill as a checklist or starting reference, but do not copy the WebSocket, file upload, Docker, or CI snippets verbatim. Require authenticated WebSocket handshakes with authorization checks, generate server-side upload filenames with containment and content validation, and pin build tools such as uv before using the deployment templates.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
Findings (3)

T08 · Insecure Dependencies

Warning
Location
SKILL.md:713
Finding

Unpinned Build Tool Installation Creates Supply-Chain Risk

Content
View full analysis

Vulnerability Details

File Location: SKILL.md:713 and SKILL.md:816
Vulnerability Type: Unpinned third-party dependency installation
Risk Level: Medium

Vulnerable Code

Docker build template at SKILL.md:713:

dockerfile
RUN pip install --no-cache-dir uv

GitHub Actions template at SKILL.md:816:

yaml
- run: pip install uv && uv sync

Technical Analysis

Both templates install uv without an exact version constraint or package hash. Consequently, the installed package is determined by the package index at build time rather than by a reviewed and reproducible dependency definition.

This allows build behavior to change without any corresponding source-code change. If a future release is defective or malicious, or if the package index or publisher account is compromised, Docker builds and CI jobs based on these templates will install and execute the affected package. The risk is amplified in CI because installation commands execute automatically and commonly have access to repository content, build artifacts, and workflow-scoped credentials.

Although uv sync --frozen protects application dependency resolution when a valid lockfile is present, it does not protect the preceding unpinned installation of the uv executable itself.

Attack Path

  1. A project adopts the supplied Dockerfile or GitHub Actions template.
  2. A malicious or compromised release of uv becomes the version resolved by pip install uv.
  3. A developer starts a Docker build, or the CI workflow runs after a push or pull request.
  4. pip downloads and installs the unreviewed release.
  5. Package installation or subsequent execution of uv sync runs attacker-controlled behavior in the build or CI environment.
  6. Depending on environment permissions, that behavior may read source code, alter build artifacts, access available workflow credentials, or compromise the resulting container image.

...[truncated 674 chars]

Remediation
View remediation

Remediation Suggestions

  1. Pin uv to a reviewed exact version in both templates, for example:
    dockerfile
    RUN pip install --no-cache-dir "uv==<reviewed-version>"
    
  2. Where supported, download from a controlled artifact repository and verify an expected cryptographic checksum.
  3. Use pip hash checking with a locked bootstrap requirements file rather than resolving the installer dynamically.
  4. Keep uv.lock under version control and continue using uv sync --frozen.
  5. Run automated dependency updates through reviewed pull requests rather than automatically consuming the latest release.
  6. Restrict CI token permissions and avoid exposing deployment secrets to dependency-installation jobs.
  7. Apply the same pinned version consistently at both SKILL.md:713 and SKILL.md:816.

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:918
Finding

Client-Controlled Upload Filename Enables Path Traversal

Content
View full analysis

Vulnerability Details

File Location: SKILL.md:918-935
Vulnerability Type: Path traversal through an untrusted upload filename
Risk Level: High

Vulnerable Code

python
from fastapi import UploadFile, File

@router.post("/upload")
async def upload_file(
    file: UploadFile = File(..., description="File to upload"),
    user: User = Depends(get_current_user),
):
    # Validate
    if file.size and file.size > 10 * 1024 * 1024:  # 10MB
        raise ValidationError("File too large (max 10MB)")
    
    allowed_types = {"image/jpeg", "image/png", "application/pdf"}
    if file.content_type not in allowed_types:
        raise ValidationError(f"File type not allowed: {file.content_type}")
    
    # Save
    contents = await file.read()
    path = f"uploads/{user.id}/{file.filename}"
    # Save to S3/local storage...
    
    return {"filename": file.filename, "size": len(contents)}

Technical Analysis

UploadFile.filename is supplied by the client and is inserted directly into a storage path. The code does not remove path separators, reject traversal components, generate a server-controlled object name, or verify that the resolved destination remains inside the intended upload directory.

If the placeholder is completed with a normal filesystem write, filenames containing components such as ../ can resolve outside uploads/{user.id}. Absolute paths or platform-specific separators may create similar problems, depending on the operating system and path-handling API.

The MIME allowlist does not mitigate path traversal. The content_type value is also client-controlled and does not prove that the content is a valid image or PDF. In addition, checking file.size before reading does not reliably impose a hard streaming limit when the size is absent or inaccurate.

Attack Path

  1. An attacker authenticates as an ordinary user because the endpoint requires `get_curren ...[truncated 1564 chars]
Remediation
View remediation

Remediation Suggestions

  1. Do not use the client filename as the storage key. Generate a random server-side identifier, such as a UUID, and map it to separately stored display metadata.
  2. If retaining part of the original filename is necessary, strip all directory components and enforce a strict allowlist of characters and extensions.
  3. Build paths with pathlib.Path, resolve the candidate destination, and verify that it remains under a fixed resolved upload root before opening it.
  4. Open new files with exclusive creation semantics where possible to prevent unintended overwrites.
  5. Store uploads outside application source, static roots, and executable directories.
  6. Verify actual file signatures and decoded content rather than trusting the multipart content_type.
  7. Enforce the byte limit while streaming the body and abort as soon as the maximum is exceeded.
  8. Apply restrictive service-account and storage permissions so uploaded content cannot modify application or system files.
  9. For object storage, generate server-controlled keys and configure bucket policies to prevent execution and public access by default.

T05 · Unauthorized Access and Privilege Escalation

Error
Location
SKILL.md:889
Finding

WebSocket Endpoint Permits User Identity Spoofing

Content
View full analysis

Vulnerability Details

File Location: SKILL.md:889-914
Vulnerability Type: Missing WebSocket authentication and object-level authorization
Risk Level: High

Vulnerable Code

python
from fastapi import WebSocket, WebSocketDisconnect

class ConnectionManager:
    def __init__(self):
        self.connections: dict[str, WebSocket] = {}
    
    async def connect(self, user_id: str, ws: WebSocket):
        await ws.accept()
        self.connections[user_id] = ws
    
    def disconnect(self, user_id: str):
        self.connections.pop(user_id, None)
    
    async def send(self, user_id: str, message: dict):
        if ws := self.connections.get(user_id):
            await ws.send_json(message)

manager = ConnectionManager()

@router.websocket("/ws/{user_id}")
async def websocket_endpoint(websocket: WebSocket, user_id: str):
    await manager.connect(user_id, websocket)
    try:
        while True:
            data = await websocket.receive_json()
            # Process message
    except WebSocketDisconnect:
        manager.disconnect(user_id)

Technical Analysis

The endpoint treats the path parameter user_id as the connection's authenticated identity. It does not validate a session or token, derive the identity from a trusted principal, or check whether the caller is authorized to use the requested identifier.

ConnectionManager.connect() then stores the socket directly under that caller-controlled identifier. Assigning to self.connections[user_id] also silently replaces any existing connection for the same user. Messages sent through send(user_id, message) are delivered to whichever socket most recently claimed that identifier.

WebSocket connections do not automatically inherit the authentication dependency used by unrelated HTTP routes. Authentication and authorization must be explicitly performed during the WebSocket handshake. The example also lacks an ...[truncated 1629 chars]

Remediation
View remediation

Remediation Suggestions

  1. Authenticate the connection before calling websocket.accept().
  2. Validate a short-lived WebSocket credential using an approved transport, such as an authorization-capable handshake mechanism or a secure, single-use ticket.
  3. Derive user_id exclusively from the validated principal; do not trust a path parameter as identity.
  4. Perform object-level authorization before joining any user-specific channel.
  5. Validate the Origin header against an environment-specific allowlist for browser clients.
  6. Define an explicit policy for duplicate sessions rather than silently replacing an existing connection.
  7. Associate connection records with unique connection IDs and validated principal objects, not only user-controlled strings.
  8. Authorize every inbound message according to the authenticated principal and validate its schema before processing.
  9. Add tests proving that unauthenticated connections, identity mismatches, unauthorized channels, and untrusted origins are rejected.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (5)

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 138)May include surrounding context.

md
raise ValueError(f"environment must be one of {allowed}")
        return v
    
    model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}

@lru_cache
def get_settings() -> Settings:

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The Quick Start section uses broad natural-language trigger phrases such as 'audit my FastAPI project' and 'set up a new FastAPI project' that could be matched too loosely by an agent platform and cause unintended invocation. In a skill-dispatch system, overly generic triggers can activate this skill in contexts where the user did not explicitly request it, leading to confusing behavior, context hijacking, or incorrect automation selection.

Content

No source excerpt is available for this finding.

Internal Network Request

Medium
Category
Server-Side Request Forgery
Confidence
70% confidence
Finding

Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Content

Scanner excerpt · SKILL.md (reported line 736)May include surrounding context.

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --retries=3
CMD ["python", "-c", "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"]

CMD ["uvicorn", "src.app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

text

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
84% confidence
Finding

The file upload example validates size and MIME type but then reads and saves user-supplied content using a path derived from the original filename, without warning about storage safety. This can normalize insecure handling patterns, including unsafe persistence of untrusted files, path manipulation risks, and missing malware/content scanning or filename sanitization.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The skill exposes many generic natural-language triggers such as 'set up a new FastAPI project' and 'review my API security' without clear scoping, confirmation, or exclusion conditions. In an agent setting, this can cause overbroad activation and unintended execution of powerful code-generation or security-review behaviors on the wrong target or with insufficient user intent verification.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.