#3695·harness

Blind SSRF via git ls-remote in Gitspace lookup-repo endpoint

Author: geo-chenCreated Jun 13, 2026Updated Jun 13, 2026

Summary

The POST /api/v1/gitspaces/lookup-repo endpoint accepts a user-supplied url field and passes it directly to git ls-remote without validating whether the URL targets a private or internal IP address. Any authenticated user with InfraProviderView permission (a low-privilege role) can use this to make the Harness server issue outbound TCP/HTTP connections to arbitrary hosts including internal network addresses and cloud metadata endpoints (e.g., 169.254.169.254), enabling blind SSRF and internal port scanning.

Details

Vulnerable path:

app/api/handler/gitspace/lookup_repo.go -> HandleLookupRepo -> app/api/controller/gitspace/lookup_repo.go -> Controller.LookupRepo -> app/gitspace/scm/scm.go -> SCM.CheckValidCodeRepo -> detectBranch(ctx, url) -> detectDefaultGitBranch(ctx, url) -> git ls-remote --symref -q <attacker-url> HEAD

The sanitizeLookupRepoInput function at app/api/controller/gitspace/lookup_repo.go (lines 78-93) only validates that the URL parses successfully and has a non-empty scheme. It does not check whether the resolved host is a loopback, link-local, or private RFC-1918 address:

go
func (c *Controller) sanitizeLookupRepoInput(in *LookupRepoInput) error {
    if in.RepoType == "" && in.URL == "" {
        return ErrRepoMissing
    }
    parsedURL, err := url.Parse(in.URL)
    if err != nil {
        return ErrInvalidURL
    }
    if parsedURL.Scheme == "" {
        return ErrBadURLScheme
    }
    // NO private IP check
    return nil
}

detectDefaultGitBranch (scm.go lines 113-130) then runs:

go
cmd := command.New("ls-remote",
    command.WithFlag("--symref"),
    command.WithFlag("-q"),
    command.WithArg(gitRepoDir),  // attacker URL
    command.WithArg("HEAD"),
)

git ls-remote makes an actual TCP connection and HTTP GET to <url>/info/refs?service=git-upload-pack. The resulting error distinguishes open from closed ports (see PoC below), providing a port-scanning oracle.

Contrast with the webhook service (app/services/webhook/http_client.go), which explicitly blocks private IPs via a post-DNS DialContext check -- that guard is entirely absent in the gitspace SCM code path.

PoC

(available upon request)

Impact

An authenticated attacker with InfraProviderView access can:

  1. Enumerate open TCP ports on internal hosts (distinguishable error messages for open vs. closed ports).
  2. Reach cloud instance metadata endpoints (169.254.169.254, fd00:ec2::254) to probe for credential exposure depending on metadata endpoint behavior.
  3. Send HTTP requests with git-specific headers to any internal HTTP service reachable from the server.

In multi-tenant Harness installations, any project member can trigger the SSRF against the internal network hosting the Harness deployment (AWS VPC, GCP VPC, Kubernetes cluster internal CIDRs, etc.).