CVE-2022-29188 (Go, published 2022-05-21T00:15Z)
Description: value: Smokescreen is an HTTP proxy. The primary use case for Smokescreen is to prevent server-side request forgery (SSRF) attacks in which external attackers leverage the behavior of applications to connect to or scan internal infrastructure. Smokescreen also offers an option to deny access to additional (e.g., external) URLs by way of a deny list. There was an issue in Smokescreen that made it possible to bypass the deny list feature by surrounding the hostname with square brackets (e.g. `[example.com]`). This only impacted the HTTP proxy functionality of Smokescreen. HTTPS requests were not impacted. Smokescreen version 0.0.4 contains a patch for this issue.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
Repository: https://github.com/stripe/smokescreen
Files changed:
pkg/smokescreen/smokescreen.go
pkg/smokescreen/smokescreen_test.go
pkg/smokescreen/testdata/acl.yaml
Commit message: Fix hostname parsing for square brackets


Vulnerable code:
		remoteHost := req.Host
	sctx.decision, sctx.lookupTime, pctx.Error = checkIfRequestShouldBeProxied(config, pctx.Req, pctx.Req.Host)
func TestUnsafeAllowPrivateRanges (t *testing.T) {
	a.NoError(conf.SetDenyRanges([]string {"192.168.0.0/24", "10.0.0.0/8"}))

    version: v1
    services:
      - name: test-trusted-srv
        project: security
        action: enforce
        allowed_domains:
          - notarealhost.test
          - httpbin.org
      - name: test-local-srv
        project: security
        action: open
        allowed_domains:
          - 127.0.0.1

Fixed code:
		remoteHost := req.URL.Hostname()
	sctx.decision, sctx.lookupTime, pctx.Error = checkIfRequestShouldBeProxied(config, pctx.Req, pctx.Req.URL.Hostname())
func TestUnsafeAllowPrivateRanges(t *testing.T) {
	a.NoError(conf.SetDenyRanges([]string{"192.168.0.0/24", "10.0.0.0/8"}))
var hostSquareBracketsCases = []struct {
	scheme    string
	proxyType string
}{
	{"http", "http"},
	{"https", "connect"},
}

func TestHostSquareBrackets(t *testing.T) {
	for _, testCase := range hostSquareBracketsCases {
		t.Run(testCase.scheme, func(t *testing.T) {
			a := assert.New(t)
			r := require.New(t)

			cfg, err := testConfig("test-open-srv")
			require.NoError(t, err)
			logHook := proxyLogHook(cfg)

			proxySrv := proxyServer(cfg)
			defer proxySrv.Close()

			// Create a http.Client that uses our proxy
			client, err := proxyClient(proxySrv.URL)
			r.NoError(err)

			resp, err := client.Get(fmt.Sprintf("%s://[stripe.com]", testCase.scheme))
			if err != nil {
				r.Contains(err.Error(), "Request rejected by proxy")
			} else {
				r.Equal(http.StatusProxyAuthRequired, resp.StatusCode)
			}

			entry := findCanonicalProxyDecision(logHook.AllEntries())
			r.NotNil(entry)

			if a.Contains(entry.Data, "allow") {
				a.Equal(false, entry.Data["allow"])
				a.Equal("host matched rule in global deny list", entry.Data["decision_reason"])
			}
			if a.Contains(entry.Data, "proxy_type") {
				a.Contains(entry.Data["proxy_type"], testCase.proxyType)
			}
		})
	}
}

version: v1
services:
  - name: test-trusted-srv
    project: security
    action: enforce
    allowed_domains:
      - notarealhost.test
      - httpbin.org
  - name: test-local-srv
    project: security
    action: open
    allowed_domains:
      - 127.0.0.1
  - name: test-open-srv
    project: security
    action: open

global_deny_list:
  - stripe.com