Skip to content

codetabs Proxy Endpoint Non-Blind SSRF #46

@360AlphaLab

Description

@360AlphaLab

Vulnerability Report: codetabs Proxy Endpoint Non-Blind SSRF

Vulnerability Submission Assessment

Vulnerability Chain Exists: Yes

Submission Verdict

Attribute Value
Vulnerability Type A10: Server-Side Request Forgery (SSRF)
Exploitation Method Unauthenticated
CVSS 3.1 8.6 High (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:L)
Submit for Report Yes

Rationale

Based on the rule in 4.5.1 SSRF Direct Impact Determination: "SSRF via proxy/import functionality with response echo → Submit", and the rule in Step 2: SSRF Specific Determination: "Able to access internal services and return data → Submit", this vulnerability meets the submission criteria.
Additionally, the quick-reference appendix explicitly lists SSRF + Cloud Metadata / Internal Network Echo as high-severity and reportable.

Details

The vulnerability exists in a publicly accessible proxy endpoint. An attacker can specify an arbitrary target address via the quest parameter without authentication. The server then issues an HTTP request to that address and returns the response body directly.
This means an attacker can read data from internal HTTP services, localhost services, and cloud metadata endpoints — going beyond mere port scanning and constituting a full data-exfiltration SSRF.

Next Steps

Proceed to generate the complete vulnerability report.

1. Product Overview

codetabs is a multi-functional HTTP API service implemented in Go, providing capabilities such as code statistics, proxying, header retrieval, geolocation, weather, and random number generation. The project exposes multiple /v1/ routes via http.ServeMux, among which /v1/proxy/ is responsible for forwarding requests to target URLs and returning the results.

2. Vulnerability Description

Attribute Value
Vulnerability Type CWE-918: Server-Side Request Forgery (SSRF)
CVSS 3.1 8.6 High (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:L)
Vulnerable File project/codetabs/proxy/proxy.go (22103)
Prerequisites No login required, no special privileges needed
Entry Point /v1/proxy/ (public endpoint, unauthenticated)
  • Core Impact: An attacker can leverage the server's network position to access internal networks, localhost, or cloud metadata addresses and directly read the response content.
  • Environmental Constraints: The target service must have proxy.Router enabled, and the server must have network reachability to the target address.
  • Default Trigger Conditions: The route /v1/proxy/ is registered by default. In the current version, isBanned() always returns false, meaning exploitation conditions are satisfied by default.

3. Affected Scope

  • Affected Versions: The version corresponding to the current project code, at minimum any version containing the implementation in project/codetabs/proxy/proxy.go.
  • Unaffected Versions: Versions that do not expose the /v1/proxy/ route, or that have added target address allowlisting, internal/metadata address blocking, or have disabled arbitrary proxy capability.
  • Trigger Conditions and Defaults:
    • Route enabled: Enabled by default.
    • Authentication requirement: None.
    • Target restrictions: No host, IP, port, or protocol restrictions.
    • Redirect restrictions: Not configured.

4. Vulnerability Details

4.1 Code Audit Analysis

The proxy route is registered in main.go:

mux.HandleFunc("/v1/proxy/", mw(proxy.Router, "proxy", c))

Although the middleware calls isBanned(r) for the proxy service, the current implementation of isBanned() always returns false:

func isBanned(r *http.Request) bool {
	return false
}

This means there is no effective access control in the current version, and any external user can request this endpoint.

In proxy.Router(), the application reads the attacker-controlled quest value directly from the query parameters:

r.ParseForm()
p.quest = r.Form.Get("quest")

Subsequently, in doProxyRequest(), only the protocol prefix is stripped and re-prepended:

p.quest = "http://" + u.RemoveProtocolFromURL(p.quest)

RemoveProtocolFromURL() merely removes the http:// or https:// prefix and does not validate whether the target address belongs to an internal network, loopback address, or cloud metadata address:

func RemoveProtocolFromURL(url string) string {
	if strings.HasPrefix(url, "https://") {
		return url[8:]
	}
	if strings.HasPrefix(url, "https:/") {
		return url[7:]
	}
	if strings.HasPrefix(url, "http://") {
		return url[7:]
	}
	if strings.HasPrefix(url, "http:/") {
		return url[6:]
	}
	return url
}

Finally, the request is issued directly by the server:

var netClient = &http.Client{
	Timeout: time.Second * 10,
}
resp, err := netClient.Get(p.quest)

No CheckRedirect is set here, so Go's default behavior of automatically following redirects applies, which can be used to assist SSRF via redirect chains.

The response logic also performs no security filtering:

Key code snippet:

if strings.Contains(contentType, "application/json") {
	json.NewDecoder(resp.Body).Decode(&data)
	if data != nil {
		w.Header().Set("Content-Type", "application/json")
		u.SendJSONToClient(w, data, 200)
		return
	}
}

reader := bufio.NewReader(resp.Body)
for {
	line, err := reader.ReadBytes('\n')
	if err != nil {
		if err != io.EOF {
			log.Printf("Error PROXY-2: %s => %v\n", p.quest, err)
		}
		w.Write([]byte(fmt.Sprintf("%v", string(line))))
		return
	}
	w.Write([]byte(fmt.Sprintf("%v", string(line))))
}

The vulnerability chain can be summarized as:

External request to /v1/proxy/?quest=<target address>
  → Server reads quest
  → Strips protocol prefix and prepends http://
  → Server issues GET request to attacker-specified address
  → Target response content is returned directly to attacker
  → Non-blind SSRF achieved

4.2 PoC Construction

This endpoint is designed as a "proxy content fetcher," making it an ideal SSRF verification point. Constructing a PoC only requires controlling the quest parameter — no additional authentication or special request headers are needed.

Key payload design points:

  1. The target address does not need to include a protocol; the server will automatically prepend http://.
  2. Localhost addresses can be used directly, e.g., 127.0.0.1:80/.
  3. Cloud metadata addresses can be used, e.g., 169.254.169.254/latest/meta-data/.
  4. If the target returns JSON, the endpoint will parse and re-return the JSON, demonstrating response echo.
  5. If the target returns plain text, the endpoint outputs the text line by line, suitable for reading internal service pages or debug endpoints.
  6. Since Go follows redirects by default, an externally controlled site returning 302 Location: http://127.0.0.1:8080/ can also be used to assist SSRF.

5. Proof of Concept (Reproduction)

5.1 Environment Setup

  • Target: codetabs
  • Frontend address: http://127.0.0.1:3000/
  • Reproduction method: Direct access to the proxy endpoint
  • Authentication state: Not logged in

5.2 Reproduction Steps

Step 1: Confirm the proxy endpoint is accessible

Access http://127.0.0.1:3000/v1/proxy/?quest=127.0.0.1:80/. The server will issue a request to 127.0.0.1:80 on localhost.
If a web service exists on the target port, the page content will appear directly in the response; if no service exists, an error indicating the resource is unavailable will be returned.

Step 2: Verify ability to read cloud metadata or internal network resources

Access http://127.0.0.1:3000/v1/proxy/?quest=169.254.169.254/latest/meta-data/.
If the runtime environment can reach the cloud metadata endpoint, the response will directly display metadata paths or specific content.
Similarly, this can be replaced with an internal HTTP service address, e.g., 10.0.0.5:8080/ or 192.168.1.10:8080/health.

Step 3: Verify that response content is visible to the attacker

Point quest at an internal endpoint that returns JSON, e.g., 127.0.0.1:8080/debug.
After the server requests that address, if the response Content-Type is application/json, the data will be parsed and re-output as JSON.
If the response is plain text, it will be output verbatim. This directly proves the vulnerability is a non-blind SSRF rather than mere connectivity probing.

5.3 Result Verification

Verification Item Result
Request quest=127.0.0.1:80/ Server issues request to localhost; if an HTTP service is running, its page content is returned
Request quest=169.254.169.254/latest/meta-data/ If the environment can reach the cloud metadata address, content is directly readable
Request internal JSON endpoint Response data is re-encoded as JSON by the server and returned
Request text-based internal page Response body is returned line by line; attacker can view content directly
Redirect-assisted SSRF Since CheckRedirect is not set on the client, automatic redirect following is supported by default

5.4 Attack Chain Diagram

Attacker
  → Accesses /v1/proxy/?quest=<internal or metadata address>
  → Server issues request
  → Internal target returns response
  → Proxy endpoint returns response content to attacker
  → SSRF confirmed

6. POC

A Python verification script has been generated in the repository: poc_codetabs_proxy_ssrf.py

Command-line examples:

python .\poc_codetabs_proxy_ssrf.py --base-url http://127.0.0.1:3000 --target-url 127.0.0.1:80/
python .\poc_codetabs_proxy_ssrf.py --base-url http://127.0.0.1:3000 --target-url 169.254.169.254/latest/meta-data/

Equivalent raw request examples:

curl "http://127.0.0.1:3000/v1/proxy/?quest=127.0.0.1:80/"
curl "http://127.0.0.1:3000/v1/proxy/?quest=169.254.169.254/latest/meta-data/"

7. Remediation Recommendations

  1. Remove arbitrary proxy capability
    If the business does not require fetching from arbitrary targets, take down the /v1/proxy/ route entirely.

  2. Enforce a strict allowlist
    Only allow access to explicitly allowlisted domains; prohibit users from submitting arbitrary URLs.
    Allowlist validation should be performed after URL parsing, based on the normalized hostname and resolved IP addresses.

  3. Block access to internal, loopback, link-local, and cloud metadata addresses
    The following targets must be blocked:

    • 127.0.0.0/8
    • 10.0.0.0/8
    • 172.16.0.0/12
    • 192.168.0.0/16
    • 169.254.0.0/16
    • ::1, fc00::/7, fe80::/10
    • Cloud provider metadata-specific addresses
  4. Restrict protocols and ports
    Only allow https; prohibit dangerous protocols such as file, gopher, and ftp.
    Only allow access to ports required by the business.

  5. Disable automatic redirect following
    Set CheckRedirect on http.Client to prevent bypassing allowlists or probing strategies via 302 redirects.

  6. Do not echo target responses directly
    If proxy capability must be retained, consider returning only a fixed-format summary rather than the full response body, to avoid turning it into a "data-exfiltration SSRF."

  7. Audit and strengthen access control
    The current isBanned() always returns false and provides no real protection. Rate limiting, authentication, logging/alerting, and source control should be added.

Remediation Example

parsedURL, err := url.ParseRequestURI(userInput)
if err != nil {
	return errors.New("invalid url")
}

host := parsedURL.Hostname()
ips, err := net.LookupIP(host)
if err != nil || len(ips) == 0 {
	return errors.New("unresolvable host")
}

for _, ip := range ips {
	if isPrivateOrLoopback(ip) || isMetadataIP(ip) {
		return errors.New("forbidden target")
	}
}

client := &http.Client{
	Timeout: 5 * time.Second,
	CheckRedirect: func(req *http.Request, via []*http.Request) error {
		return http.ErrUseLastResponse
	},
}

This report contains no screenshots as required; evidence is based on source code location, exploitation path, and an executable PoC.

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