Skip to content

[Bug]: REST task/request id sanitization #805

Description

@bartek-w

What happened?

In rest transport we use f'/v1/tasks/{request.id}'
request.id in theory can contain ../../ or other substrings.

Relevant log output

Code of Conduct

  • I agree to follow this project's Code of Conduct

Activity

  1. added theissue type on Mar 11, 2026
  2. mtsmyassin commented on Apr 17, 2026

    @mtsmyassin

    Proposed Fix: Validate task/request IDs in REST transport

    The REST transport exposes endpoints like GET /v1/tasks/{task_id} where task_id comes directly from the URL path. Since these IDs are later used to look up tasks in the store (and potentially in file paths or database keys), accepting values like ../secret or ../../etc/passwd could be dangerous.

    Root Cause

    Starlette path parameters are URL-decoded by the time they reach the handler, so a request to /v1/tasks/..%2F..%2Fconfig would arrive as ../../config. Without sanitization this string is passed verbatim to task_store.get().

    Suggested Fix

    Add a shared validator function and apply it as a Starlette dependency (or inline check) at the REST handler layer:

    import re
    from a2a.types import InvalidParamsError
    from a2a.utils.errors import ServerError
    
    # Only allow characters that are safe in a task/request ID:
    # alphanumerics, hyphens, underscores, dots (but not leading dots)
    _SAFE_ID_RE = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9_\-\.]{0,127}$')
    
    def validate_task_id(task_id: str) -> str:
        """Validates that a task/request ID is safe to use."""
        if not task_id or not _SAFE_ID_RE.match(task_id):
            raise ServerError(
                error=InvalidParamsError(
                    message=f'Invalid task ID format: {task_id!r}'
                )
            )
        return task_id

    This can be called at the top of every REST handler that accepts a path parameter:

    async def get_task(request: Request) -> Response:
        task_id = validate_task_id(request.path_params["task_id"])
        # ...

    Alternative: Starlette Path Regex Constraint

    Starlette also supports regex constraints directly in route definitions, which catches the issue before it even reaches handler code:

    Route("/v1/tasks/{task_id:str}", endpoint=get_task)
    # becomes:
    Route(r"/v1/tasks/{task_id:[a-zA-Z0-9][a-zA-Z0-9_\-\.]{0,127}}", endpoint=get_task)

    However, this only works if path parameters are defined with regex syntax, which requires Starlette ≥ 0.20. The inline validate_task_id() approach is more portable and gives a better error message to clients.

    Notes

    • The same validation should apply to context_id if it appears as a path parameter.
      • JSON-RPC transport is not affected because IDs come in the request body and Pydantic already validates them as strings; the attack surface is the URL path.
        • The InvalidParamsError response ensures spec-compliant error responses (-32602).
          Happy to submit a PR for this if it would be helpful.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions