Skip to content

HEAD requests still stream the full object from GCS #59

@MN755

Description

@MN755

Summary

main.go handles HEAD through the same streaming path as GET, so a HEAD /bucket/large-object request still opens a GCS reader and copies the full object body even though the caller only asked for headers.

Repro

  1. Run gcsproxy against a bucket with a large object.
  2. Send HEAD /<bucket>/<object> (or HEAD /<object> in -bucket mode).
  3. The router accepts HEAD in handler().
  4. proxy() then falls through to streamObject() or streamRange() exactly like GET.
  5. streamObject() calls NewReader(...) and io.Copy(w, objr), and streamRange() does the same with NewRangeReader(...).

Expected

HEAD should return the same metadata as GET without reading and streaming the object body from GCS.

Actual

The HEAD path still performs the full backend read, so large objects are downloaded from GCS for header-only requests.

Likely Cause

These paths never branch on r.Method == http.MethodHead:

  • main.go, handler() registers both object routes with Methods("GET", "HEAD")
  • main.go, proxy() sends any matching request into streamRange() / streamObject()
  • main.go, streamObject() unconditionally does NewReader(...) + io.Copy(...)
  • main.go, streamRange() unconditionally does NewRangeReader(...) + io.Copy(...)

Source / Trigger

Attacker- or client-controlled request method: any HEAD request to an existing object, including a ranged HEAD.

Control Gap

The code correctly limits methods to GET and HEAD, but it never gives HEAD its header-only semantics before opening the GCS object reader.

Impact

This makes HEAD as expensive as GET on the GCS side. Large objects still incur backend read latency and bandwidth/billing, and caches/load balancers that probe with HEAD do not get the cheap metadata path they expect.

Fix Direction

Handle HEAD before the body copy path:

  • fetch attrs as you do now,
  • set the response headers,
  • write the status,
  • and return without creating NewReader(...) / NewRangeReader(...) or calling io.Copy(...).

I would also add focused HEAD tests next to the current GET/range coverage so this does not regress.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions