Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reject requets with no tokenized data #21

Merged
merged 2 commits into from
Jul 26, 2024
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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The client configures their HTTP library to use the tokenizer service as it's HT

```ruby
conn = Faraday.new(
proxy: "http://tokenizer.flycast",
proxy: "http://tokenizer.flycast",
headers: {
proxy_tokenizer: Base64.encode64(sealed_secret),
proxy_authorization: "Bearer trustno1"
Expand Down Expand Up @@ -99,7 +99,7 @@ seal_key = ENV["TOKENIZER_PUBLIC_KEY"]
sealed_secret = RbNaCl::Boxes::Sealed.new(seal_key).box(secret.to_json)

processor_params = {
dst: "X-Stripe-Token",
dst: "X-Stripe-Token",
fmt: "token=%s"
}

Expand All @@ -110,7 +110,7 @@ conn.get("http://api.stripe.com")

## Host allowlist

If a client is fully compromised, the attacker could send encrypted secrets via tokenizer to a service that simply echoes back the request. This way, the attacker could learn the plaintext value of the secret. To mitigate against this, secrets can specify which hosts they may be used against.
If a client is fully compromised, the attacker could send encrypted secrets via tokenizer to a service that simply echoes back the request. This way, the attacker could learn the plaintext value of the secret. To mitigate against this, secrets can specify which hosts they may be used against.

```ruby
secret = {
Expand Down Expand Up @@ -193,4 +193,5 @@ Tokenizer is configured with the following environment variables:

- `OPEN_KEY` - The hex encoded 32 byte private key is used for decrypting secrets.
- `LISTEN_ADDRESS` - The address (`ip:port`) to listen on.
- `FILTERED_HEADERS` - A comma separated list of request headers to strip from client requests.
- `FILTERED_HEADERS` - A comma separated list of request headers to strip from client requests.
- `OPEN_PROXY` - Setting `1` or `true` will allow requests that don't contain sealed secrets to be proxied. Such requests are blocked by default.
5 changes: 5 additions & 0 deletions cmd/tokenizer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/sirupsen/logrus"
"github.com/superfly/tokenizer"
"golang.org/x/exp/slices"
)

// Package variables can be overridden at build time:
Expand Down Expand Up @@ -85,6 +86,10 @@ func runServe() {
tkz.ProxyHttpServer.Logger = logrus.StandardLogger()
}

if slices.Contains([]string{"1", "true"}, os.Getenv("OPEN_PROXY")) {
tkz.OpenProxy = true
}

server := &http.Server{Handler: tkz}

go func() {
Expand Down
13 changes: 12 additions & 1 deletion tokenizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const headerProxyTokenizer = "Proxy-Tokenizer"

type tokenizer struct {
*goproxy.ProxyHttpServer

// OpenProxy dictates whether requests without any sealed secrets are allowed.
OpenProxy bool

priv *[32]byte
pub *[32]byte
}
Expand All @@ -59,7 +63,7 @@ func NewTokenizer(openKey string) *tokenizer {
proxy := goproxy.NewProxyHttpServer()
tkz := &tokenizer{ProxyHttpServer: proxy, priv: priv, pub: pub}

tkz.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "I'm not that kind of server")
})

Expand Down Expand Up @@ -156,6 +160,13 @@ func (t *tokenizer) HandleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*ht
processors = append(processors, reqProcessors...)
}

if len(processors) == 0 && !t.OpenProxy {
pud.reqLog.Warn("no processors")
return nil, errorResponse(ErrBadRequest)
}

pud.reqLog = pud.reqLog.WithField("processors", len(processors))

for _, processor := range processors {
if err := processor(req); err != nil {
pud.reqLog.WithError(err).Warn("run processor")
Expand Down
25 changes: 11 additions & 14 deletions tokenizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,33 +47,30 @@ func TestTokenizer(t *testing.T) {
tkzServer := httptest.NewServer(tkz)
defer tkzServer.Close()

client, err := Client(tkzServer.URL)
req, err := http.NewRequest(http.MethodPost, appURL+"/foo", nil)
assert.NoError(t, err)

req, err := http.NewRequest(http.MethodPost, appURL+"/foo", nil)
auth := "trustno1"
token := "supersecret"
secret, err := (&Secret{AuthConfig: NewBearerAuthConfig(auth), ProcessorConfig: &InjectProcessorConfig{Token: token}}).Seal(sealKey)
assert.NoError(t, err)

// TLS error (proxy doesn't trust upstream)
client, err := Client(tkzServer.URL, WithAuth(auth), WithSecret(secret, nil))
assert.NoError(t, err)
resp, err := client.Get(appURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadGateway, resp.StatusCode)

// make proxy trust upstream
UpstreamTrust.AddCert(appServer.Certificate())

// no proxy auth or secrets (good)
assert.Equal(t, &echoResponse{
Headers: http.Header{},
Body: "",
}, doEcho(t, client, req))

// bad proxy auth doesn't matter if no secrets are used
client, err = Client(tkzServer.URL, WithAuth("bogus"))
// error if no secrets
client, err = Client(tkzServer.URL, WithAuth(auth))
assert.NoError(t, err)
resp, err = client.Get(appURL)
assert.NoError(t, err)
assert.Equal(t, &echoResponse{
Headers: http.Header{}, // proxy auth isn't leaked downstream
Body: "",
}, doEcho(t, client, req))
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

t.Run("inject processor", func(t *testing.T) {
auth := "trustno1"
Expand Down
Loading