Skip to content

fix(core): validate credential server before sending the key - #4

Open
Dextheking1 wants to merge 3 commits into
localhost8083:mars-desktopfrom
Dextheking1:validate-server-url
Open

Dextheking1 wants to merge 3 commits into
localhost8083:mars-desktopfrom
Dextheking1:validate-server-url

Conversation

@Dextheking1

@Dextheking1 Dextheking1 commented Oct 23, 2026 •

Copy link
Copy Markdown

Issue for this PR

No linked issue.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

serverUrl() returns credential.metadata.server verbatim, and modelsFor() sends the API key as a bearer to {server}/v1/models every time the catalog loads.

Nothing validates the value. The legacy auth.json import copies credential metadata into the stored credential as-is, so a server value from an imported file (or any future writer of that field) points the key at an arbitrary host, over plain http if it says so. Upstream validates its equivalent value before use (normalizeServer in opencode.ts).

Fix: parse the value in serverUrl(). Only https passes, plus http on loopback (localhost, 127.0.0.1, ::1). Anything else falls back to the default server. Trailing slash and /v1 are stripped the same way the default server is normalized, and the result is rebuilt from origin + path, so userinfo and query in the value are dropped.

How did you verify your code works?

Verified: ran the new function isolated, 18 cases (https ok, http off loopback rejected, loopback http ok, unparseable rejected, /v1 stripped, userinfo dropped). Patched mars.ts transpiles clean with bun build.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Summary by Sourcery

Prevent API keys from being sent to untrusted or insecure Mars servers by validating and normalizing configured endpoints.

Bug Fixes:

  • Validate credential and environment-configured Mars server URLs before using API keys, allowing HTTPS and loopback HTTP only and falling back safely for invalid values.
  • Prevent legacy credential imports from retaining file-controlled server overrides.

Enhancements:

  • Normalize accepted server URLs by removing trailing slashes and the /v1 suffix while excluding userinfo and query components.

serverUrl returned credential.metadata.server verbatim and
modelsFor sends the API key to it as a bearer token. Parse the
value first: https only, http on loopback, anything else falls
back to the default server.
@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

The PR hardens credential server selection by parsing and restricting server URLs before model catalog requests, preventing imported or otherwise untrusted credentials from sending API keys to arbitrary hosts or insecure remote endpoints.

Sequence diagram for validated credential server selection

sequenceDiagram
    participant Catalog as CatalogLoader
    participant Models as modelsFor
    participant Server as serverUrl
    participant API as CredentialServer

    Catalog->>Models: modelsFor(http, value)
    Models->>Server: serverUrl(value)
    Server->>Server: Parse raw with URL
    alt HTTPS or loopback HTTP
        Server-->>Models: normalized origin and path
        Models->>API: GET /v1/models with bearer API key
        API-->>Models: ModelsResponse
    else Invalid, insecure, or unparseable server
        Server-->>Models: defaultServer
        Models->>API: GET /v1/models with bearer API key
        API-->>Models: ModelsResponse
    end
Loading

File-Level Changes

Change Details Files
Validate and normalize the credential-provided server URL before it can receive the bearer token.
  • Reject non-HTTPS URLs, allowing HTTP only for localhost and loopback IPs.
  • Fall back to the default server for missing, malformed, or unsafe values.
  • Normalize the accepted URL by removing trailing slashes and a terminal /v1, rebuilding it from origin and pathname to exclude userinfo and query data.
packages/core/src/plugin/provider/mars.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • No issue referenced. Please add Closes #<number> linking to the relevant issue.

Please edit this PR description to address the above within 72 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="packages/core/src/plugin/provider/mars.ts" line_range="35" />
<code_context>
+  try {
+    const url = new URL(raw)
+    const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]"
+    if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return defaultServer
+    return `${url.origin}${url.pathname.replace(/\/+$/, "").replace(/\/v1$/i, "")}`
+  } catch {
</code_context>
<issue_to_address>
**HTTPS servers can steal API keys**

When an attacker-controlled legacy `auth.json` is imported with `metadata.server` set to an HTTPS host they control, `serverUrl` accepts the supplied HTTPS origin, and `modelsFor` attaches `value.key` as a bearer token; the catalog request therefore sends the key to the attacker-controlled host.

Restrict credential servers to trusted hosts, or require explicit user approval before sending the key to a new host.
</issue_to_address>

### Comment 2
<location path="packages/core/src/plugin/provider/mars.ts" line_range="35" />
<code_context>
+  try {
+    const url = new URL(raw)
+    const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]"
+    if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return defaultServer
+    return `${url.origin}${url.pathname.replace(/\/+$/, "").replace(/\/v1$/i, "")}`
+  } catch {
</code_context>
<issue_to_address>
**API keys travel over plaintext**

When `MARSAI_URL` is set to a remote `http://` endpoint and the credential's server is absent or rejected, `serverUrl` returns the unvalidated `defaultServer`, and `modelsFor` attaches `value.key` as a bearer token to its `/v1/models` request, exposing the key in transit.

Validate `MARSAI_URL` with the same HTTPS-or-loopback-HTTP policy before allowing it to serve as the fallback.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and a rejected or misparsed custom server silently falls back to the default endpoint, so requests may send the credential there instead of to the configured server. Reverting restores the old routing, but it cannot recall a key already transmitted.

Blocking findings: packages/core/src/plugin/provider/mars.ts:35, packages/core/src/plugin/provider/mars.ts:35


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread packages/core/src/plugin/provider/mars.ts Outdated
Comment thread packages/core/src/plugin/provider/mars.ts Outdated
defaultServer took MARSAI_URL as-is, so a remote http:// value
became the fallback and the key went to it in plaintext whenever
the credential had no server of its own. Run it through the same
normalizeServer check as credential servers; on failure use the
hardcoded https default.
The import copied legacy auth.json metadata into the stored key
credential verbatim. No first-party writer puts a server on a key
credential (the opencode console flow writes it on OAuth
credentials of its own integration), so the field only ever
arrives as file-controlled text. Drop it during the import;
imported keys use the configured default server. The serverUrl
validation stays as a second layer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant