Repository: oauth2-proxy/oauth2-proxy (2024-02-24T21:25:12Z)

Issue:
[Bug]: Regression in alpha config upstreams with regex (+rewrite target)
### OAuth2-Proxy Version

7.6.0

### Provider

google

### Expected Behaviour

In 7.5.1 we were running the following upstreamConfig

```
      upstreamConfig:
        upstreams:
          - id: app
            path: ^/(.*)$
            rewriteTarget: /$1
            proxyWebSockets: true
            uri: http://app:3000
          - id: api
            path: ^/api(/stripe/webhook|/graphql|/downloads)(.*)$
            rewriteTarget: /api$1$2
            proxyWebSockets: true
            uri: http://api:5555
```

Where all our requests went to the app except a few prefixed by /api/graphql for example.

### Current Behaviour

Upgrading to 7.6.0, this no longer works. All requests to the app end up returning the html served on / (in our example the base html from NextJS, but all js/css/etc also returns that base html) In a similar fashion, all things accessing the api component return the /api endpoint on the /api route.

I've tried with httpbin to see if is was on our side, but also here it can only serve / and not any other routes.

The output in the logs is the same as in 7.5.1

![CleanShot 2024-02-15 at 11 27 44@2x](https://github.com/oauth2-proxy/oauth2-proxy/assets/16780/c44778ee-80a5-4737-9e9a-05f66fff091f)

In this screenshot both / and /spec.js return the contents of /

I saw some more logger calls in the go code but i'm unsure on how to get these printed out.

- Changing the container version to 7.5.1 and all works again as it should
- the -alpine container of 7.6.0 has the same issue, so it is not the image itself
- I've scanned all commits that went into 7.6.0 but cannot find any change that is related, my guess is that it might have been one of the dependency updates

### Steps To Reproduce

1. helm install the 7.5.1 version
2. configure the upstreams like above
3. WORKS
4. upgrade the helm chart (image goes to 7.6.0
5. ERROR
6. change just the image version of the deployment to 7.5.1
7. WORKS

### Possible Solutions

_No response_

### Configuration details or additional information

_No response_


Discussion:
having the same problem with:

```
          upstreams:
            - id: web
              path: ^/(.*)$
              rewriteTarget: /$1
              uri: http://kubernetes-dashboard-web:8000/
            - id: api
              path: ^/api/(.*)$
              rewriteTarget: /api/$1
              uri: http://kubernetes-dashboard-api:9000/
```

This works as expected in 7.5.1 but in 7.6.0 I get a 404 not found
I encountered the same issue and discovered that it stems from the use of the envsubst package, which requires escaping the dollar sign $ with a double dollar sign $$.

For reference, you can see the relevant code here: [oauth2-proxy implementation](https://github.com/oauth2-proxy/oauth2-proxy/blob/e3dc927e570700ae598425344cf07073965bd19c/pkg/apis/options/load.go#L170).

It's important to clarify that the solution provided below is a workaround and not a permanent fix. The underlying issue with the envsubst package remains, and this approach simply allows us to bypass the problem temporarily.
To address this in the context mentioned by @sled, the workaround would involve modifying the configuration as follows:

```yaml
upstreams:
  - id: web
    path: ^/(.*)$$
    rewriteTarget: /$$1
    uri: http://kubernetes-dashboard-web:8000/
  - id: api
    path: ^/api/(.*)$$
    rewriteTarget: /api/$$1
    uri: http://kubernetes-dashboard-api:9000/
```

This ensures that the $ symbols are properly escaped, allowing for the correct interpretation of the YAML configuration.



This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.
Just used a half a day trying to understand why our software didn't work after upgrading from 7.5.1 to 7.6.0. Turns out it was this change. 

If you are not planning to fix this in short term, this should be at least documented.
any updates? 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b2ffc50dff..50dbc9b915 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
 - [#2605](https://github.com/oauth2-proxy/oauth2-proxy/pull/2605) fix: show login page on broken cookie (@Primexz)
 - [#2743](https://github.com/oauth2-proxy/oauth2-proxy/pull/2743) feat: allow use more possible google admin-sdk api scopes (@BobDu)
 - [#2359](https://github.com/oauth2-proxy/oauth2-proxy/pull/2359) feat: add SourceHut (sr.ht) provider(@bitfehler)
+-[#2524](https://github.com/oauth2-proxy/oauth2-proxy/pull/2524) fix: regex substitution for $ signs in upstream path handling before running envsubst (@dashkan / @tuunit)
 
 # V7.10.0
 
diff --git a/pkg/apis/options/load.go b/pkg/apis/options/load.go
index b198c4ffad..c302e8e775 100644
--- a/pkg/apis/options/load.go
+++ b/pkg/apis/options/load.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"os"
 	"reflect"
+	"regexp"
 	"strings"
 
 	"github.com/a8m/envsubst"
@@ -155,7 +156,8 @@ func LoadYAML(configFileName string, into interface{}) error {
 	return nil
 }
 
-// Performs the heavy lifting of the LoadYaml function
+// loadAndParseYaml reads the config from the filesystem and
+// execute the environment variable substitution
 func loadAndParseYaml(configFileName string) ([]byte, error) {
 	if configFileName == "" {
 		return nil, errors.New("no configuration file provided")
@@ -166,12 +168,26 @@ func loadAndParseYaml(configFileName string) ([]byte, error) {
 		return nil, fmt.Errorf("unable to load config file: %w", err)
 	}
 
-	// We now parse over the yaml with env substring, and fill in the ENV's
-	buffer, err := envsubst.Bytes(unparsedBuffer)
+	modifiedBuffer, err := normalizeSubstitution(unparsedBuffer)
+	if err != nil {
+		return nil, fmt.Errorf("error normalizing substitution string : %w", err)
+	}
+
+	buffer, err := envsubst.Bytes(modifiedBuffer)
 	if err != nil {
 		return nil, fmt.Errorf("error in substituting env variables : %w", err)
 	}
 
 	return buffer, nil
+}
+
+// normalizeSubstitution normalizes dollar signs ($) with numerals like
+// $1 or $2 properly by correctly escaping them
+func normalizeSubstitution(unparsedBuffer []byte) ([]byte, error) {
+	unparsedString := string(unparsedBuffer)
+
+	regexPattern := regexp.MustCompile(`\$(\d+)`)
 
+	substitutedString := regexPattern.ReplaceAllString(unparsedString, `$$$$1`)
+	return []byte(substitutedString), nil
 }


Test patch:
diff --git a/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go
index fefbc2e795..06123c37f0 100644
--- a/pkg/apis/options/load_test.go
+++ b/pkg/apis/options/load_test.go
@@ -487,6 +487,31 @@ sub:
 					StringOption: "Bob",
 				},
 			}),
+			Entry("with a config file containing $ signs for things other than environment variables", loadYAMLTableInput{
+				configFile: []byte(`
+stringOption: /$1
+stringSliceOption:
+- /$1
+- ^/(.*)$
+- api/$1
+- api/(.*)$
+- ^/api/(.*)$
+- /api/$1`),
+				input: &TestOptions{},
+				expectedOutput: &TestOptions{
+					StringOption: "/$1",
+					TestOptionSubStruct: TestOptionSubStruct{
+						StringSliceOption: []string{
+							"/$1",
+							"^/(.*)$",
+							"api/$1",
+							"api/(.*)$",
+							"^/api/(.*)$",
+							"/api/$1",
+						},
+					},
+				},
+			}),
 		)
 	})