Skip to content

fix(a2a-server): enforce authentication and stop checkpoint path traversal - #28699

Open
herdiyana256 wants to merge 1 commit into
google-gemini:mainfrom
herdiyana256:fix-a2a-server-missing-auth-and-path-traversal
Open

fix(a2a-server): enforce authentication and stop checkpoint path traversal#28699
herdiyana256 wants to merge 1 commit into
google-gemini:mainfrom
herdiyana256:fix-a2a-server-missing-auth-and-path-traversal

Conversation

@herdiyana256

Copy link
Copy Markdown

Summary

The A2A server's custom REST routes (/tasks, /executeCommand, /listCommands, /tasks/:taskId/metadata) are registered directly on the Express app in createApp() and never go through the configured UserBuilder at all, so they accept requests with no credentials whatsoever. Separately, @a2a-js/sdk's own DefaultRequestHandler only checks user.isAuthenticated for the authenticated-extended-card method, not for message/send and other task-driving JSON-RPC methods, so customUserBuilder was never actually enforced there either. On top of that, the two example credentials it checked against ('valid-token', 'admin:password') are hardcoded literals in public source. Net effect: the agent card's declared bearerAuth/basicAuth securitySchemes were not enforced anywhere, on a service the docs (docs/core/remote-agents.md) describe deploying as a public Cloud Run endpoint.

Verified locally: a built a2a-server accepted POST /tasks, POST /executeCommand, POST / (message/send), and GET /listCommands with zero Authorization header at all.

Separately, RestoreCommand builds the checkpoint file path from the executeCommand request body's args via path.join(checkpointDir, selectedFile) without validating selectedFile, so a name such as ../../other-project/checkpoints/some-real-checkpoint.json escapes checkpointDir. Since matching checkpoint content is echoed back in the response, this discloses the contents of any other project's checkpoint file on the host (which routinely contains full conversation/tool-call history). Verified with a crafted sibling checkpoint file: the traversal read its content back through /executeCommand with no authentication.

Fix

  • Add an authentication middleware applied ahead of every route (public agent card excepted) that checks credentials against CODER_AGENT_BEARER_TOKEN / CODER_AGENT_BASIC_USERNAME / CODER_AGENT_BASIC_PASSWORD using a constant-time comparison, and fails closed (rejects everything) when unconfigured rather than falling back to a hardcoded default.
  • RestoreCommand: reject any requested checkpoint name that isn't equal to its own path.basename (checkpoint filenames are always flat, see generateCheckpointFileName), before it ever reaches the filesystem.
  • Updated app.test.ts / endpoints.test.ts to authenticate via the new env-configured bearer token, and added regression tests for: no-credentials rejection on every custom route and the JSON-RPC endpoint, rejection of the old hardcoded-style credential, and traversal rejection in restore.test.ts.

Test plan

  • npx vitest run --root packages/a2a-server — 154/154 passing
  • npm run typecheck --workspace=@google/gemini-cli-a2a-server
  • npx eslint on all changed files — clean
  • Manual PoC against a locally built+running server: confirmed /tasks, /executeCommand, /listCommands, and POST / (message/send) all return 401 with no credentials post-fix (previously all succeeded); confirmed a valid configured bearer token still authenticates correctly; confirmed the checkpoint traversal PoC is rejected with "Invalid checkpoint name" both with and without valid credentials.

…ersal

The A2A server's custom REST routes (/tasks, /executeCommand,
/listCommands, /tasks/:taskId/metadata) are registered directly on the
Express app and never go through the configured UserBuilder at all, so
they accept any request with no credentials whatsoever. Separately, the
A2A SDK's own request handler only checks user.isAuthenticated for the
authenticated-extended-card method, not for message/send and other
task-driving JSON-RPC methods, so the customUserBuilder wired into
createApp() was never actually enforced there either -- and the two
example credentials it checked against ('valid-token',
'admin:password') were hardcoded literals baked into public source.
Net effect: the agent card's declared bearerAuth/basicAuth
securitySchemes were not enforced anywhere, on a service documented to
be deployed as a public Cloud Run endpoint.

Add an authentication middleware applied ahead of every route (public
agent card excepted) that checks credentials against
CODER_AGENT_BEARER_TOKEN / CODER_AGENT_BASIC_USERNAME /
CODER_AGENT_BASIC_PASSWORD using a constant-time comparison, and fails
closed (rejects everything) when unconfigured rather than falling back
to a hardcoded default.

Separately, RestoreCommand builds the checkpoint file path from the
executeCommand request body's args via path.join(checkpointDir,
selectedFile) without validating selectedFile, so a name such as
"../../other-project/checkpoints/some-real-checkpoint.json" escapes
checkpointDir and reads (and, since matching checkpoints are echoed
back in the response, discloses the contents of) any other project's
checkpoint file on the host. Checkpoint filenames are always flat
(see generateCheckpointFileName), so reject any requested name that
isn't equal to its own path.basename.
@herdiyana256
herdiyana256 requested a review from a team as a code owner August 5, 2026 13:26
@github-actions github-actions Bot added the size/l A large sized PR label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

📊 PR Size: size/L

  • Lines changed: 253
  • Additions: +217
  • Deletions: -36
  • Files changed: 5

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses critical security vulnerabilities in the A2A server. It enforces authentication across all custom REST routes and the JSON-RPC endpoint, replacing hardcoded credentials with environment-based configuration. Additionally, it secures the checkpoint restoration process by sanitizing input paths to prevent directory traversal attacks, ensuring that sensitive data cannot be disclosed.

Highlights

  • Authentication Enforcement: Implemented a mandatory authentication middleware for all A2A server routes (excluding the public agent card) using environment-configured credentials and constant-time comparison to prevent timing attacks.
  • Path Traversal Prevention: Updated the RestoreCommand to validate checkpoint filenames, rejecting any paths containing directory separators to prevent unauthorized access to files outside the designated directory.
  • Test Suite Updates: Enhanced test coverage by adding regression tests for authentication requirements and path traversal scenarios, and updated existing E2E tests to include valid credentials.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces security enhancements to the a2a-server package, including directory traversal protection in the restore command and robust authentication middleware enforcing bearer token or basic authentication across endpoints. Constant-time comparisons are used to prevent timing attacks, and tests have been updated accordingly. Feedback highlights a potential timing leak in the basic authentication logic where the short-circuiting && operator could allow username enumeration; evaluating both username and password comparisons beforehand is recommended to ensure a true constant-time check.

Comment on lines +133 to +138
if (
safeCompare(user, expectedUser) &&
safeCompare(password, expectedPassword)
) {
return { userName: 'basic-user', isAuthenticated: true };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The use of the && operator here introduces a short-circuit timing leak. If the username comparison (safeCompare(user, expectedUser)) fails, the password comparison is skipped entirely. This allows an attacker to potentially enumerate valid usernames by measuring the response time. To ensure a true constant-time comparison, evaluate both comparisons before combining them.

      const userMatch = safeCompare(user, expectedUser);
      const passwordMatch = safeCompare(password, expectedPassword);
      if (userMatch && passwordMatch) {
        return { userName: 'basic-user', isAuthenticated: true };
      }

@gemini-cli gemini-cli Bot added the status/need-issue Pull requests that need to have an associated issue. label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l A large sized PR status/need-issue Pull requests that need to have an associated issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant