go.lang.security.injection.tainted-url-host.tainted-url-host

profile photo of returntocorpreturntocorp
Author
unknown
Download Count*

User data flows into the host portion of this manually-constructed URL. This could allow an attacker to send data to their own server, potentially exposing sensitive data such as cookies or authorization information sent with this request. They could also probe internal servers or other resources that the server runnig this code can access. (This is called server-side request forgery, or SSRF.) Do not allow arbitrary hosts. Instead, create an allowlist for approved hosts hardcode the correct host.

Run Locally

Run in CI

Defintion

rules:
  - id: tainted-url-host
    languages:
      - go
    message: User data flows into the host portion of this manually-constructed URL.
      This could allow an attacker to send data to their own server, potentially
      exposing sensitive data such as cookies or authorization information sent
      with this request. They could also probe internal servers or other
      resources that the server runnig this code can access. (This is called
      server-side request forgery, or SSRF.) Do not allow arbitrary hosts.
      Instead, create an allowlist for approved hosts hardcode the correct host.
    metadata:
      cwe:
        - "CWE-918: Server-Side Request Forgery (SSRF)"
      owasp:
        - A10:2021 - Server-Side Request Forgery (SSRF)
      references:
        - https://goteleport.com/blog/ssrf-attacks/
      category: security
      technology:
        - go
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      confidence: MEDIUM
      cwe2022-top25: true
      cwe2021-top25: true
      subcategory:
        - vuln
      impact: MEDIUM
      likelihood: MEDIUM
    mode: taint
    pattern-sinks:
      - patterns:
          - pattern-either:
              - patterns:
                  - pattern-either:
                      - pattern: fmt.Fprintf($F, "$URLSTR", ...)
                      - pattern: fmt.Sprintf("$URLSTR", ...)
                      - pattern: fmt.Printf("$URLSTR", ...)
                  - metavariable-regex:
                      metavariable: $URLSTR
                      regex: http(s?)://%(v|s|q).*
              - patterns:
                  - pattern: '"$URLSTR" + ...'
                  - metavariable-regex:
                      metavariable: $URLSTR
                      regex: \w+://.*
    pattern-sources:
      - patterns:
          - pattern-either:
              - pattern: |
                  ($REQUEST : *http.Request).$ANYTHING
              - pattern: |
                  ($REQUEST : http.Request).$ANYTHING
          - metavariable-regex:
              metavariable: $ANYTHING
              regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$
    severity: WARNING

Examples

tainted-url-host.go

package main

import (
    "crypto/tls"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
)

func handlerIndex(w http.ResponseWriter, r *http.Request) {
    tr := &http.Transport{
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        }

    client := &http.Client{Transport: tr}

    if r.Method == "POST" && r.URL.Path == "/api" {
        // ruleid: tainted-url-host
        url := fmt.Sprintf("https://%v/api", r.URL.Query().Get("proxy"))

        resp, err := client.Post(url, "application/json", r.Body)

        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }

        defer resp.Body.Close()

        if resp.StatusCode != 200 {
            w.WriteHeader(500)
            return
        }

        w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy"))))
        return
    } else {
        proxy := r.URL.Query()["proxy"]
        secure := r.URL.Query()["secure"]

        if (secure) {
            // ruleid: tainted-url-host
            url := fmt.Sprintf("https://%s", proxy)
        } else {
            // ruleid: tainted-url-host
            url := fmt.Sprintf("http://%q", proxy)
        }
        resp, err := client.Post(url, "application/json", r.Body)
    }
}

func handlerOther(w http.ResponseWriter, r *http.Request) {
    tr := &http.Transport{
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        }

    client := &http.Client{Transport: tr}

    if r.Method == "POST" && r.URL.Path == "/api" {
        // ruleid: tainted-url-host
        url := fmt.Printf("https://%v/api", r.URL.Query().Get("proxy"))

        resp, err := client.Post(url, "application/json", r.Body)

        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }

        defer resp.Body.Close()

        if resp.StatusCode != 200 {
            w.WriteHeader(500)
            return
        }

        w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy"))))
        return
    } else {
        proxy := r.URL.Query()["proxy"]
        secure := r.URL.Query()["secure"]

        if (secure) {
            // ruleid: tainted-url-host
            fmt.Fprintf(w, "https://%s", proxy)
        } else {
            // ruleid: tainted-url-host
            fmt.Fprintf(w, "http://%q", proxy)
        }
        resp, err := client.Post(url, "application/json", r.Body)
    }
}

func handlerOk(w http.ResponseWriter, r *http.Request) {
    tr := &http.Transport{
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        }

    client := &http.Client{Transport: tr}

    if r.Method == "POST" && r.URL.Path == "/api" {
        // ok: tainted-url-host
        url := fmt.Printf("https://example.com/%v", r.URL.Query().Get("proxy"))

        resp, err := client.Post(url, "application/json", r.Body)

        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }

        defer resp.Body.Close()

        if resp.StatusCode != 200 {
            w.WriteHeader(500)
            return
        }

        w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy"))))
        return
    } else {
        proxy := r.URL.Query()["proxy"]
        secure := r.URL.Query()["secure"]

        if (secure) {
            // ok: tainted-url-host
            url := fmt.Sprintf("https://example.com/%s", proxy)
        } else {
            // ok: tainted-url-host
            fmt.Fprintf(w, "http://example.com/%q", proxy)
        }
        resp, err := client.Post(url, "application/json", r.Body)
    }
}

func newRedirectServer(addr string, rootPath string) *http.Server {
    return &http.Server{
        Addr: addr,
        Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
            // ruleid: tainted-url-host
            target := "https://" + req.Host
            if rootPath != "" {
                target += "/" + strings.TrimRight(strings.TrimLeft(rootPath, "/"), "/")
            }
            target += req.URL.Path
            if len(req.URL.RawQuery) > 0 {
                target += "?" + req.URL.RawQuery
            }
            http.Redirect(w, req, target, http.StatusTemporaryRedirect)
        }),
    }
}

func main() {
    http.HandleFunc("/", handlerIndex)
    http.HandleFunc("/other", handleOther)
    http.HandleFunc("/ok", handleOk)
    http.ListenAndServe(":8888", nil)
}