Skip to content

Commit 5d23615

Browse files
authored
fix: scope Ory-Base-URL-Rewrite* headers to Ory-bound requests (#446)
* fix: scope Ory-Base-URL-Rewrite* headers to Ory-bound requests The ory proxy/tunnel reverse proxy attached the Ory-No-Custom-Domain-Redirect, Ory-Base-URL-Rewrite, and Ory-Base-URL-Rewrite-Token headers to every outbound request, including those forwarded to the developer's own upstream app. The last header carries a temporary project API key, which is only consumed by Ory and has no reason to reach the upstream. Scope these headers to Ory-bound requests only as a defense-in-depth hardening, and add a regression test. * fix: strip client-supplied Ory-* headers before re-applying Prevents clients from spoofing Ory-Base-URL-Rewrite*, Ory-Base-URL-Rewrite-Token, and Ory-No-Custom-Domain-Redirect headers. Spoofed headers would otherwise be forwarded unchanged to the developer's upstream app, or — when no API key is configured — passed through to Ory.
1 parent f9247ab commit 5d23615

2 files changed

Lines changed: 149 additions & 22 deletions

File tree

cmd/cloudx/proxy/helpers.go

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -194,28 +194,7 @@ func runReverseProxy(ctx context.Context, h *client.CommandHelper, stdErr io.Wri
194194
PathPrefix: "",
195195
}, nil
196196
},
197-
proxy.WithReqMiddleware(func(r *httputil.ProxyRequest, c *proxy.HostConfig, body []byte) ([]byte, error) {
198-
if r.Out.URL.Host == oryURL.Host {
199-
r.Out.URL.Path = strings.TrimPrefix(r.Out.URL.Path, conf.pathPrefix)
200-
r.Out.Host = oryURL.Host
201-
} else if conf.rewriteHost {
202-
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
203-
r.Out.Host = c.UpstreamHost
204-
}
205-
206-
publicURL := conf.publicURL
207-
if conf.pathPrefix != "" {
208-
publicURL = urlx.AppendPaths(publicURL, conf.pathPrefix)
209-
}
210-
211-
r.Out.Header.Set("Ory-No-Custom-Domain-Redirect", "true")
212-
r.Out.Header.Set("Ory-Base-URL-Rewrite", publicURL.String())
213-
if len(apiKey) > 0 {
214-
r.Out.Header.Set("Ory-Base-URL-Rewrite-Token", apiKey)
215-
}
216-
217-
return body, nil
218-
}),
197+
proxy.WithReqMiddleware(reqMiddleware(conf, oryURL, apiKey)),
219198
proxy.WithRespMiddleware(func(resp *http.Response, config *proxy.HostConfig, body []byte) ([]byte, error) {
220199
l, err := resp.Location()
221200
if err == nil {
@@ -313,6 +292,44 @@ and configure your SDKs to point to it, for example in JavaScript:
313292
return nil
314293
}
315294

295+
// reqMiddleware returns the request middleware used by the reverse proxy. The
296+
// Ory-* headers (including the temporary API key in Ory-Base-URL-Rewrite-Token)
297+
// are only attached to Ory-bound requests. Requests forwarded to the developer's
298+
// upstream application do not need — and must not receive — these headers.
299+
func reqMiddleware(conf *config, oryURL *url.URL, apiKey string) proxy.ReqMiddleware {
300+
return func(r *httputil.ProxyRequest, c *proxy.HostConfig, body []byte) ([]byte, error) {
301+
// Strip any client-supplied Ory-* headers before selectively re-applying
302+
// them below. Otherwise a client could spoof these headers: they would be
303+
// forwarded unchanged to the developer's upstream app, or — when apiKey is
304+
// empty — an attacker-supplied Ory-Base-URL-Rewrite-Token would be passed
305+
// through to Ory.
306+
r.Out.Header.Del("Ory-No-Custom-Domain-Redirect")
307+
r.Out.Header.Del("Ory-Base-URL-Rewrite")
308+
r.Out.Header.Del("Ory-Base-URL-Rewrite-Token")
309+
310+
if r.Out.URL.Host == oryURL.Host {
311+
r.Out.URL.Path = strings.TrimPrefix(r.Out.URL.Path, conf.pathPrefix)
312+
r.Out.Host = oryURL.Host
313+
314+
publicURL := conf.publicURL
315+
if conf.pathPrefix != "" {
316+
publicURL = urlx.AppendPaths(publicURL, conf.pathPrefix)
317+
}
318+
319+
r.Out.Header.Set("Ory-No-Custom-Domain-Redirect", "true")
320+
r.Out.Header.Set("Ory-Base-URL-Rewrite", publicURL.String())
321+
if len(apiKey) > 0 {
322+
r.Out.Header.Set("Ory-Base-URL-Rewrite-Token", apiKey)
323+
}
324+
} else if conf.rewriteHost {
325+
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
326+
r.Out.Host = c.UpstreamHost
327+
}
328+
329+
return body, nil
330+
}
331+
}
332+
316333
func newJWTSigner() (jose.Signer, *jose.JSONWebKeySet, error) {
317334
key, err := jwksx.GenerateSigningKeys(
318335
uuid.Must(uuid.NewV4()).String(),

cmd/cloudx/proxy/helpers_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright © 2023 Ory Corp
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package proxy
5+
6+
import (
7+
"net/http"
8+
"net/http/httputil"
9+
"net/url"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/ory/x/proxy"
16+
)
17+
18+
func TestReqMiddleware(t *testing.T) {
19+
oryURL := &url.URL{Scheme: "https", Host: "example.projects.oryapis.com"}
20+
publicURL := &url.URL{Scheme: "http", Host: "localhost:4000"}
21+
const apiKey = "ory_apikey_sentinel"
22+
23+
const (
24+
headerToken = "Ory-Base-URL-Rewrite-Token"
25+
headerRewrite = "Ory-Base-URL-Rewrite"
26+
headerNoCustom = "Ory-No-Custom-Domain-Redirect"
27+
)
28+
29+
newRequest := func(t *testing.T, outHost string) *httputil.ProxyRequest {
30+
t.Helper()
31+
in, err := http.NewRequest(http.MethodGet, "http://localhost:4000/foo", nil)
32+
require.NoError(t, err)
33+
out, err := http.NewRequest(http.MethodGet, "http://"+outHost+"/foo", nil)
34+
require.NoError(t, err)
35+
return &httputil.ProxyRequest{In: in, Out: out}
36+
}
37+
38+
t.Run("case=ory-bound request receives Ory headers", func(t *testing.T) {
39+
conf := &config{publicURL: publicURL}
40+
r := newRequest(t, oryURL.Host)
41+
42+
_, err := reqMiddleware(conf, oryURL, apiKey)(r, &proxy.HostConfig{}, nil)
43+
require.NoError(t, err)
44+
45+
assert.Equal(t, apiKey, r.Out.Header.Get(headerToken))
46+
assert.Equal(t, publicURL.String(), r.Out.Header.Get(headerRewrite))
47+
assert.Equal(t, "true", r.Out.Header.Get(headerNoCustom))
48+
})
49+
50+
t.Run("case=upstream-bound request does not receive Ory headers", func(t *testing.T) {
51+
conf := &config{publicURL: publicURL}
52+
r := newRequest(t, "localhost:3000")
53+
54+
_, err := reqMiddleware(conf, oryURL, apiKey)(r, &proxy.HostConfig{}, nil)
55+
require.NoError(t, err)
56+
57+
assert.Empty(t, r.Out.Header.Get(headerToken), "API key must not leak to the upstream app")
58+
assert.Empty(t, r.Out.Header.Get(headerRewrite))
59+
assert.Empty(t, r.Out.Header.Get(headerNoCustom))
60+
})
61+
62+
t.Run("case=rewriteHost upstream sets X-Forwarded-Host but not the API key", func(t *testing.T) {
63+
conf := &config{publicURL: publicURL, rewriteHost: true}
64+
r := newRequest(t, "localhost:3000")
65+
66+
_, err := reqMiddleware(conf, oryURL, apiKey)(r, &proxy.HostConfig{UpstreamHost: "upstream.internal"}, nil)
67+
require.NoError(t, err)
68+
69+
assert.Equal(t, r.In.Host, r.Out.Header.Get("X-Forwarded-Host"))
70+
assert.Equal(t, "upstream.internal", r.Out.Host)
71+
assert.Empty(t, r.Out.Header.Get(headerToken), "API key must not leak to the upstream app")
72+
})
73+
74+
t.Run("case=client-spoofed Ory headers are stripped from upstream-bound requests", func(t *testing.T) {
75+
conf := &config{publicURL: publicURL}
76+
r := newRequest(t, "localhost:3000")
77+
r.Out.Header.Set(headerToken, "ory_apikey_spoofed")
78+
r.Out.Header.Set(headerRewrite, "http://evil.example")
79+
r.Out.Header.Set(headerNoCustom, "true")
80+
81+
_, err := reqMiddleware(conf, oryURL, apiKey)(r, &proxy.HostConfig{}, nil)
82+
require.NoError(t, err)
83+
84+
assert.Empty(t, r.Out.Header.Get(headerToken), "spoofed token must not be forwarded to the upstream app")
85+
assert.Empty(t, r.Out.Header.Get(headerRewrite), "spoofed header must not be forwarded to the upstream app")
86+
assert.Empty(t, r.Out.Header.Get(headerNoCustom), "spoofed header must not be forwarded to the upstream app")
87+
})
88+
89+
t.Run("case=client-spoofed token is stripped from Ory-bound requests when apiKey is empty", func(t *testing.T) {
90+
conf := &config{publicURL: publicURL}
91+
r := newRequest(t, oryURL.Host)
92+
r.Out.Header.Set(headerToken, "ory_apikey_spoofed")
93+
94+
_, err := reqMiddleware(conf, oryURL, "")(r, &proxy.HostConfig{}, nil)
95+
require.NoError(t, err)
96+
97+
assert.Empty(t, r.Out.Header.Get(headerToken), "spoofed token must not be passed through to Ory when apiKey is empty")
98+
})
99+
100+
t.Run("case=real apiKey overwrites a client-spoofed token on Ory-bound requests", func(t *testing.T) {
101+
conf := &config{publicURL: publicURL}
102+
r := newRequest(t, oryURL.Host)
103+
r.Out.Header.Set(headerToken, "ory_apikey_spoofed")
104+
105+
_, err := reqMiddleware(conf, oryURL, apiKey)(r, &proxy.HostConfig{}, nil)
106+
require.NoError(t, err)
107+
108+
assert.Equal(t, apiKey, r.Out.Header.Get(headerToken), "genuine key must overwrite any spoofed token")
109+
})
110+
}

0 commit comments

Comments
 (0)