Skip to content

Commit

Permalink
Reject requets with no tokenized data (#21)
Browse files Browse the repository at this point in the history
* reject requetss with no tokenized data

* make open-proxy configurable
  • Loading branch information
btoews authored Jul 26, 2024
1 parent 9525286 commit 68cc350
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 19 deletions.
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

0 comments on commit 68cc350

Please sign in to comment.