Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions cmd/cloudx/proxy/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ func runReverseProxy(ctx context.Context, h *client.CommandHelper, stdErr io.Wri
oryURL := client.CloudAPIsURL(project.Slug + ".projects")
oryURL.Host = strings.TrimSuffix(oryURL.Host, ":443")

rateLimitName, rateLimitValue, _ := client.RateLimitHeader()

writer := herodot.NewJSONWriter(&errorLogger{Writer: stdErr})
mw := negroni.New()

Expand Down Expand Up @@ -194,7 +196,7 @@ func runReverseProxy(ctx context.Context, h *client.CommandHelper, stdErr io.Wri
PathPrefix: "",
}, nil
},
proxy.WithReqMiddleware(reqMiddleware(conf, oryURL, apiKey)),
proxy.WithReqMiddleware(reqMiddleware(conf, oryURL, apiKey, rateLimitName, rateLimitValue)),
proxy.WithRespMiddleware(respMiddleware(conf)),
))

Expand Down Expand Up @@ -272,10 +274,11 @@ and configure your SDKs to point to it, for example in JavaScript:
}

// reqMiddleware returns the request middleware used by the reverse proxy. The
// Ory-* headers (including the temporary API key in Ory-Base-URL-Rewrite-Token)
// are only attached to Ory-bound requests. Requests forwarded to the developer's
// upstream application do not need — and must not receive — these headers.
func reqMiddleware(conf *config, oryURL *url.URL, apiKey string) proxy.ReqMiddleware {
// Ory-* headers (including the temporary API key in Ory-Base-URL-Rewrite-Token
// and the rate-limit exemption header) are only attached to Ory-bound requests.
// Requests forwarded to the developer's upstream application do not need — and
// must not receive — these headers.
func reqMiddleware(conf *config, oryURL *url.URL, apiKey, rateLimitName, rateLimitValue string) proxy.ReqMiddleware {
return func(r *httputil.ProxyRequest, c *proxy.HostConfig, body []byte) ([]byte, error) {
// Strip any client-supplied Ory-* headers before selectively re-applying
// them below. Otherwise a client could spoof these headers: they would be
Expand All @@ -300,9 +303,17 @@ func reqMiddleware(conf *config, oryURL *url.URL, apiKey string) proxy.ReqMiddle
if len(apiKey) > 0 {
r.Out.Header.Set("Ory-Base-URL-Rewrite-Token", apiKey)
}
} else if conf.rewriteHost {
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
r.Out.Host = c.UpstreamHost
if rateLimitValue != "" {
r.Out.Header.Set(rateLimitName, rateLimitValue)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else {
// The rate-limit exemption header is meaningful only to Ory Network
// and must never be forwarded to the developer's upstream application.
r.Out.Header.Del(rateLimitName)
if conf.rewriteHost {
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
r.Out.Host = c.UpstreamHost
}
}

return body, nil
Expand Down
72 changes: 66 additions & 6 deletions cmd/cloudx/proxy/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func TestReqMiddleware(t *testing.T) {
conf := &config{publicURL: publicURL}
r := newRequest(t, oryURL.Host)

_, err := reqMiddleware(conf, oryURL, apiKey)(r, &proxy.HostConfig{}, nil)
_, err := reqMiddleware(conf, oryURL, apiKey, "", "")(r, &proxy.HostConfig{}, nil)
require.NoError(t, err)

assert.Equal(t, apiKey, r.Out.Header.Get(headerToken))
Expand All @@ -165,7 +165,7 @@ func TestReqMiddleware(t *testing.T) {
conf := &config{publicURL: publicURL}
r := newRequest(t, "localhost:3000")

_, err := reqMiddleware(conf, oryURL, apiKey)(r, &proxy.HostConfig{}, nil)
_, err := reqMiddleware(conf, oryURL, apiKey, "", "")(r, &proxy.HostConfig{}, nil)
require.NoError(t, err)

assert.Empty(t, r.Out.Header.Get(headerToken), "API key must not leak to the upstream app")
Expand All @@ -177,7 +177,7 @@ func TestReqMiddleware(t *testing.T) {
conf := &config{publicURL: publicURL, rewriteHost: true}
r := newRequest(t, "localhost:3000")

_, err := reqMiddleware(conf, oryURL, apiKey)(r, &proxy.HostConfig{UpstreamHost: "upstream.internal"}, nil)
_, err := reqMiddleware(conf, oryURL, apiKey, "", "")(r, &proxy.HostConfig{UpstreamHost: "upstream.internal"}, nil)
require.NoError(t, err)

assert.Equal(t, r.In.Host, r.Out.Header.Get("X-Forwarded-Host"))
Expand All @@ -192,7 +192,7 @@ func TestReqMiddleware(t *testing.T) {
r.Out.Header.Set(headerRewrite, "http://evil.example")
r.Out.Header.Set(headerNoCustom, "true")

_, err := reqMiddleware(conf, oryURL, apiKey)(r, &proxy.HostConfig{}, nil)
_, err := reqMiddleware(conf, oryURL, apiKey, "", "")(r, &proxy.HostConfig{}, nil)
require.NoError(t, err)

assert.Empty(t, r.Out.Header.Get(headerToken), "spoofed token must not be forwarded to the upstream app")
Expand All @@ -205,7 +205,7 @@ func TestReqMiddleware(t *testing.T) {
r := newRequest(t, oryURL.Host)
r.Out.Header.Set(headerToken, "ory_apikey_spoofed")

_, err := reqMiddleware(conf, oryURL, "")(r, &proxy.HostConfig{}, nil)
_, err := reqMiddleware(conf, oryURL, "", "", "")(r, &proxy.HostConfig{}, nil)
require.NoError(t, err)

assert.Empty(t, r.Out.Header.Get(headerToken), "spoofed token must not be passed through to Ory when apiKey is empty")
Expand All @@ -216,9 +216,69 @@ func TestReqMiddleware(t *testing.T) {
r := newRequest(t, oryURL.Host)
r.Out.Header.Set(headerToken, "ory_apikey_spoofed")

_, err := reqMiddleware(conf, oryURL, apiKey)(r, &proxy.HostConfig{}, nil)
_, err := reqMiddleware(conf, oryURL, apiKey, "", "")(r, &proxy.HostConfig{}, nil)
require.NoError(t, err)

assert.Equal(t, apiKey, r.Out.Header.Get(headerToken), "genuine key must overwrite any spoofed token")
})

const (
headerRateLimit = "Ory-RateLimit-Action"
rateLimitSecret = "rate-limit-secret-sentinel"
)

t.Run("case=ory-bound request receives the rate-limit exemption header when configured", func(t *testing.T) {
conf := &config{publicURL: publicURL}
r := newRequest(t, oryURL.Host)

_, err := reqMiddleware(conf, oryURL, apiKey, headerRateLimit, rateLimitSecret)(r, &proxy.HostConfig{}, nil)
require.NoError(t, err)

assert.Equal(t, rateLimitSecret, r.Out.Header.Get(headerRateLimit))
})

t.Run("case=upstream-bound request does not receive the rate-limit exemption header", func(t *testing.T) {
conf := &config{publicURL: publicURL}
r := newRequest(t, "localhost:3000")

_, err := reqMiddleware(conf, oryURL, apiKey, headerRateLimit, rateLimitSecret)(r, &proxy.HostConfig{}, nil)
require.NoError(t, err)

assert.Empty(t, r.Out.Header.Get(headerRateLimit), "rate-limit secret must not leak to the upstream app")
})

t.Run("case=configured rate-limit header overwrites a client-supplied value on Ory-bound requests", func(t *testing.T) {
conf := &config{publicURL: publicURL}
r := newRequest(t, oryURL.Host)
r.Out.Header.Set(headerRateLimit, "client-supplied")

_, err := reqMiddleware(conf, oryURL, apiKey, headerRateLimit, rateLimitSecret)(r, &proxy.HostConfig{}, nil)
require.NoError(t, err)

assert.Equal(t, rateLimitSecret, r.Out.Header.Get(headerRateLimit), "configured secret must win over a client-supplied value")
})

t.Run("case=client-supplied rate-limit header passes through when none is configured", func(t *testing.T) {
conf := &config{publicURL: publicURL}
r := newRequest(t, oryURL.Host)
r.Out.Header.Set(headerRateLimit, "client-supplied")

_, err := reqMiddleware(conf, oryURL, apiKey, headerRateLimit, "")(r, &proxy.HostConfig{}, nil)
require.NoError(t, err)

assert.Equal(t, "client-supplied", r.Out.Header.Get(headerRateLimit), "callers may still send the header themselves")
})

t.Run("case=client-supplied rate-limit header is stripped from upstream-bound requests", func(t *testing.T) {
for _, value := range []string{rateLimitSecret, ""} {
conf := &config{publicURL: publicURL}
r := newRequest(t, "localhost:3000")
r.Out.Header.Set(headerRateLimit, "client-supplied")

_, err := reqMiddleware(conf, oryURL, apiKey, headerRateLimit, value)(r, &proxy.HostConfig{}, nil)
require.NoError(t, err)

assert.Empty(t, r.Out.Header.Get(headerRateLimit), "the header is meaningful only to Ory Network and must not reach the upstream app (configured value: %q)", value)
}
})
}
Loading