VYPR
High severity8.3NVD Advisory· Published Oct 16, 2024· Updated Apr 15, 2026

CVE-2023-32192

CVE-2023-32192

Description

A vulnerability has been identified in which unauthenticated cross-site scripting (XSS) in the API Server's public API endpoint can be exploited, allowing an attacker to execute arbitrary JavaScript code in the victim browser

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/rancher/apiserverGo
< 0.0.0-20240207153957-4fd7d821d9520.0.0-20240207153957-4fd7d821d952

Patches

6
4fd7d821d952

[2.9] Fixes (#59)

https://github.com/rancher/apiserverPeter MatseykanetsFeb 7, 2024via ghsa
3 files changed · +164 5
  • pkg/server/server_test.go+130 0 modified
    @@ -2,18 +2,24 @@ package server
     
     import (
     	"errors"
    +	"fmt"
     	"net/http"
    +	"net/http/httptest"
    +	"net/url"
    +	"strings"
     	"testing"
     
     	"github.com/golang/mock/gomock"
     	"github.com/rancher/apiserver/pkg/apierror"
    +	"github.com/rancher/apiserver/pkg/builtin"
     	"github.com/rancher/apiserver/pkg/fakes"
     	"github.com/rancher/apiserver/pkg/parse"
     	"github.com/rancher/apiserver/pkg/types"
     	"github.com/rancher/apiserver/pkg/writer"
     	"github.com/rancher/wrangler/v2/pkg/schemas"
     	"github.com/rancher/wrangler/v2/pkg/schemas/validation"
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
     	"github.com/stretchr/testify/suite"
     )
     
    @@ -360,3 +366,127 @@ func (p *ServerSuite) TestServer_CustomAPIUIResponseWriter() {
     	assert.NotNil(p.T(), w.JSURL)
     	assert.NotNil(p.T(), w.APIUIVersion)
     }
    +
    +func TestServeHTMLEscaping(t *testing.T) {
    +	const (
    +		defaultJS         = "cattle.io"
    +		defaultCSS        = "cattle.io"
    +		defaultAPIVersion = "v1/apps.daemonsets.0.0"
    +		xss               = "<script>alert('xss')</script>"
    +		alphaNumeric      = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    +		badChars          = `~!@#$%^&*()_+-=[]\{}|;':",./<>?`
    +	)
    +	xssUrl := url.URL{RawPath: xss}
    +
    +	var escapedBadChars strings.Builder
    +	for _, r := range badChars {
    +		escapedBadChars.WriteString(fmt.Sprintf("&#x%X;", r))
    +	}
    +
    +	t.Parallel()
    +	tests := []struct {
    +		name             string
    +		CSSURL           string
    +		JSURL            string
    +		APIUIVersion     string
    +		URL              string
    +		desiredContent   string
    +		undesiredContent string
    +	}{
    +		{
    +			name:           "base case no xss",
    +			CSSURL:         defaultCSS,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: "https://cattle.io/v1/apps.daemonsets",
    +		},
    +		{
    +			name:           "JSS alpha-numeric",
    +			CSSURL:         defaultCSS,
    +			JSURL:          alphaNumeric,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "JSS escaped non alpha-numeric",
    +			CSSURL:           defaultCSS,
    +			JSURL:            badChars,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "CSS alpha-numeric",
    +			CSSURL:         alphaNumeric,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "CSS escaped non alpha-numeric",
    +			CSSURL:           badChars,
    +			JSURL:            defaultJS,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "api version alpha-numeric",
    +			APIUIVersion:   alphaNumeric,
    +			URL:            "https://cattle.io/v3",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "api version escaped non alpha-numeric",
    +			APIUIVersion:     badChars,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:             "Link XSS",
    +			URL:              "https://cattle.io/v1/apps.daemonsets" + xss,
    +			undesiredContent: xss,
    +			desiredContent:   xssUrl.String(),
    +		},
    +	}
    +	for _, test := range tests {
    +		tt := test
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			respStr, err := sendTestRequest(tt.URL, tt.CSSURL, tt.JSURL, tt.APIUIVersion)
    +			require.NoError(t, err, "failed to create server")
    +			require.Contains(t, respStr, tt.desiredContent, "expected content missing from server response")
    +			if tt.undesiredContent != "" {
    +				require.NotContains(t, respStr, tt.undesiredContent, "unexpected content found in server response")
    +			}
    +		})
    +	}
    +}
    +
    +func sendTestRequest(url, cssURL, jssURL, apiUIVersion string) (string, error) {
    +	resp := httptest.NewRecorder()
    +	req := httptest.NewRequest(http.MethodGet, url, nil)
    +	// These header values are needed to get an HTML return document
    +	req.Header.Set("Accept", "*/*")
    +	req.Header.Set("User-agent", "Mozilla")
    +	srv := DefaultAPIServer()
    +	srv.CustomAPIUIResponseWriter(stringGetter(cssURL), stringGetter(jssURL), stringGetter(apiUIVersion))
    +	srv.Schemas = builtin.Schemas
    +	apiOp := &types.APIRequest{
    +		Request:  req,
    +		Response: resp,
    +		Type:     "schema",
    +	}
    +	srv.Handle(apiOp)
    +	return resp.Body.String(), nil
    +}
    +
    +func stringGetter(val string) writer.StringGetter {
    +	return func() string { return val }
    +}
    
  • pkg/urlbuilder/base.go+5 4 modified
    @@ -2,16 +2,17 @@ package urlbuilder
     
     import (
     	"bytes"
    -	"fmt"
     	"net/http"
     	"net/url"
     	"strings"
     )
     
     func ParseRequestURL(r *http.Request) string {
    -	scheme := GetScheme(r)
    -	host := GetHost(r, scheme)
    -	return fmt.Sprintf("%s://%s%s%s", scheme, host, r.Header.Get(PrefixHeader), r.URL.Path)
    +	var parsedURL url.URL
    +	parsedURL.Scheme = GetScheme(r)
    +	parsedURL.Host = GetHost(r, parsedURL.Scheme)
    +	parsedURL = *parsedURL.JoinPath(r.Header.Get(PrefixHeader), r.URL.Path)
    +	return parsedURL.String()
     }
     
     func GetHost(r *http.Request, scheme string) string {
    
  • pkg/writer/html.go+29 1 modified
    @@ -2,6 +2,7 @@ package writer
     
     import (
     	"encoding/json"
    +	"fmt"
     	"strings"
     
     	"github.com/rancher/apiserver/pkg/types"
    @@ -10,7 +11,7 @@ import (
     const (
     	JSURL          = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.js"
     	CSSURL         = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.css"
    -	DefaultVersion = "1.1.10"
    +	DefaultVersion = "1.1.11"
     )
     
     var (
    @@ -71,6 +72,11 @@ func (h *HTMLResponseWriter) write(apiOp *types.APIRequest, code int, obj interf
     		jsurl = strings.Replace(JSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     		cssurl = strings.Replace(CSSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     	}
    +
    +	// jsurl and cssurl are added to the document as attributes not entities which requires special encoding.
    +	jsurl, _ = encodeAttribute(jsurl)
    +	cssurl, _ = encodeAttribute(cssurl)
    +
     	headerString = strings.Replace(headerString, "%JSURL%", jsurl, 1)
     	headerString = strings.Replace(headerString, "%CSSURL%", cssurl, 1)
     
    @@ -89,3 +95,25 @@ func jsonEncodeURL(str string) string {
     	data, _ := json.Marshal(str)
     	return string(data)
     }
    +
    +// encodeAttribute encodes all characters with the HTML Entity &#xHH; format, including spaces, where HH represents the hexadecimal value of the character in Unicode.
    +// For example, A becomes &#x41;. All alphanumeric characters (letters A to Z, a to z, and digits 0 to 9) remain unencoded.
    +// more info: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-rules-summary
    +func encodeAttribute(raw string) (string, error) {
    +	var builder strings.Builder
    +	for _, r := range raw {
    +		if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || ('0' <= r && r <= '9') {
    +			_, err := builder.WriteRune(r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		} else {
    +			// encode non-alphanumeric rune to hex.
    +			_, err := fmt.Fprintf(&builder, "&#x%X;", r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		}
    +	}
    +	return builder.String(), nil
    +}
    
69b3c2b56f3f

[2.8] Fixes (#58)

https://github.com/rancher/apiserverPeter MatseykanetsFeb 7, 2024via ghsa
3 files changed · +164 5
  • pkg/server/server_test.go+130 0 modified
    @@ -2,18 +2,24 @@ package server
     
     import (
     	"errors"
    +	"fmt"
     	"net/http"
    +	"net/http/httptest"
    +	"net/url"
    +	"strings"
     	"testing"
     
     	"github.com/golang/mock/gomock"
     	"github.com/rancher/apiserver/pkg/apierror"
    +	"github.com/rancher/apiserver/pkg/builtin"
     	"github.com/rancher/apiserver/pkg/fakes"
     	"github.com/rancher/apiserver/pkg/parse"
     	"github.com/rancher/apiserver/pkg/types"
     	"github.com/rancher/apiserver/pkg/writer"
     	"github.com/rancher/wrangler/v2/pkg/schemas"
     	"github.com/rancher/wrangler/v2/pkg/schemas/validation"
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
     	"github.com/stretchr/testify/suite"
     )
     
    @@ -360,3 +366,127 @@ func (p *ServerSuite) TestServer_CustomAPIUIResponseWriter() {
     	assert.NotNil(p.T(), w.JSURL)
     	assert.NotNil(p.T(), w.APIUIVersion)
     }
    +
    +func TestServeHTMLEscaping(t *testing.T) {
    +	const (
    +		defaultJS         = "cattle.io"
    +		defaultCSS        = "cattle.io"
    +		defaultAPIVersion = "v1/apps.daemonsets.0.0"
    +		xss               = "<script>alert('xss')</script>"
    +		alphaNumeric      = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    +		badChars          = `~!@#$%^&*()_+-=[]\{}|;':",./<>?`
    +	)
    +	xssUrl := url.URL{RawPath: xss}
    +
    +	var escapedBadChars strings.Builder
    +	for _, r := range badChars {
    +		escapedBadChars.WriteString(fmt.Sprintf("&#x%X;", r))
    +	}
    +
    +	t.Parallel()
    +	tests := []struct {
    +		name             string
    +		CSSURL           string
    +		JSURL            string
    +		APIUIVersion     string
    +		URL              string
    +		desiredContent   string
    +		undesiredContent string
    +	}{
    +		{
    +			name:           "base case no xss",
    +			CSSURL:         defaultCSS,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: "https://cattle.io/v1/apps.daemonsets",
    +		},
    +		{
    +			name:           "JSS alpha-numeric",
    +			CSSURL:         defaultCSS,
    +			JSURL:          alphaNumeric,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "JSS escaped non alpha-numeric",
    +			CSSURL:           defaultCSS,
    +			JSURL:            badChars,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "CSS alpha-numeric",
    +			CSSURL:         alphaNumeric,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "CSS escaped non alpha-numeric",
    +			CSSURL:           badChars,
    +			JSURL:            defaultJS,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "api version alpha-numeric",
    +			APIUIVersion:   alphaNumeric,
    +			URL:            "https://cattle.io/v3",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "api version escaped non alpha-numeric",
    +			APIUIVersion:     badChars,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:             "Link XSS",
    +			URL:              "https://cattle.io/v1/apps.daemonsets" + xss,
    +			undesiredContent: xss,
    +			desiredContent:   xssUrl.String(),
    +		},
    +	}
    +	for _, test := range tests {
    +		tt := test
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			respStr, err := sendTestRequest(tt.URL, tt.CSSURL, tt.JSURL, tt.APIUIVersion)
    +			require.NoError(t, err, "failed to create server")
    +			require.Contains(t, respStr, tt.desiredContent, "expected content missing from server response")
    +			if tt.undesiredContent != "" {
    +				require.NotContains(t, respStr, tt.undesiredContent, "unexpected content found in server response")
    +			}
    +		})
    +	}
    +}
    +
    +func sendTestRequest(url, cssURL, jssURL, apiUIVersion string) (string, error) {
    +	resp := httptest.NewRecorder()
    +	req := httptest.NewRequest(http.MethodGet, url, nil)
    +	// These header values are needed to get an HTML return document
    +	req.Header.Set("Accept", "*/*")
    +	req.Header.Set("User-agent", "Mozilla")
    +	srv := DefaultAPIServer()
    +	srv.CustomAPIUIResponseWriter(stringGetter(cssURL), stringGetter(jssURL), stringGetter(apiUIVersion))
    +	srv.Schemas = builtin.Schemas
    +	apiOp := &types.APIRequest{
    +		Request:  req,
    +		Response: resp,
    +		Type:     "schema",
    +	}
    +	srv.Handle(apiOp)
    +	return resp.Body.String(), nil
    +}
    +
    +func stringGetter(val string) writer.StringGetter {
    +	return func() string { return val }
    +}
    
  • pkg/urlbuilder/base.go+5 4 modified
    @@ -2,16 +2,17 @@ package urlbuilder
     
     import (
     	"bytes"
    -	"fmt"
     	"net/http"
     	"net/url"
     	"strings"
     )
     
     func ParseRequestURL(r *http.Request) string {
    -	scheme := GetScheme(r)
    -	host := GetHost(r, scheme)
    -	return fmt.Sprintf("%s://%s%s%s", scheme, host, r.Header.Get(PrefixHeader), r.URL.Path)
    +	var parsedURL url.URL
    +	parsedURL.Scheme = GetScheme(r)
    +	parsedURL.Host = GetHost(r, parsedURL.Scheme)
    +	parsedURL = *parsedURL.JoinPath(r.Header.Get(PrefixHeader), r.URL.Path)
    +	return parsedURL.String()
     }
     
     func GetHost(r *http.Request, scheme string) string {
    
  • pkg/writer/html.go+29 1 modified
    @@ -2,6 +2,7 @@ package writer
     
     import (
     	"encoding/json"
    +	"fmt"
     	"strings"
     
     	"github.com/rancher/apiserver/pkg/types"
    @@ -10,7 +11,7 @@ import (
     const (
     	JSURL          = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.js"
     	CSSURL         = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.css"
    -	DefaultVersion = "1.1.10"
    +	DefaultVersion = "1.1.11"
     )
     
     var (
    @@ -71,6 +72,11 @@ func (h *HTMLResponseWriter) write(apiOp *types.APIRequest, code int, obj interf
     		jsurl = strings.Replace(JSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     		cssurl = strings.Replace(CSSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     	}
    +
    +	// jsurl and cssurl are added to the document as attributes not entities which requires special encoding.
    +	jsurl, _ = encodeAttribute(jsurl)
    +	cssurl, _ = encodeAttribute(cssurl)
    +
     	headerString = strings.Replace(headerString, "%JSURL%", jsurl, 1)
     	headerString = strings.Replace(headerString, "%CSSURL%", cssurl, 1)
     
    @@ -89,3 +95,25 @@ func jsonEncodeURL(str string) string {
     	data, _ := json.Marshal(str)
     	return string(data)
     }
    +
    +// encodeAttribute encodes all characters with the HTML Entity &#xHH; format, including spaces, where HH represents the hexadecimal value of the character in Unicode.
    +// For example, A becomes &#x41;. All alphanumeric characters (letters A to Z, a to z, and digits 0 to 9) remain unencoded.
    +// more info: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-rules-summary
    +func encodeAttribute(raw string) (string, error) {
    +	var builder strings.Builder
    +	for _, r := range raw {
    +		if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || ('0' <= r && r <= '9') {
    +			_, err := builder.WriteRune(r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		} else {
    +			// encode non-alphanumeric rune to hex.
    +			_, err := fmt.Fprintf(&builder, "&#x%X;", r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		}
    +	}
    +	return builder.String(), nil
    +}
    
4e102cf0d07b

[2.7] Fixes (#57)

https://github.com/rancher/apiserverPeter MatseykanetsFeb 7, 2024via ghsa
3 files changed · +164 5
  • pkg/server/server_test.go+130 0 modified
    @@ -2,18 +2,24 @@ package server
     
     import (
     	"errors"
    +	"fmt"
     	"net/http"
    +	"net/http/httptest"
    +	"net/url"
    +	"strings"
     	"testing"
     
     	"github.com/golang/mock/gomock"
     	"github.com/rancher/apiserver/pkg/apierror"
    +	"github.com/rancher/apiserver/pkg/builtin"
     	"github.com/rancher/apiserver/pkg/fakes"
     	"github.com/rancher/apiserver/pkg/parse"
     	"github.com/rancher/apiserver/pkg/types"
     	"github.com/rancher/apiserver/pkg/writer"
     	"github.com/rancher/wrangler/pkg/schemas"
     	"github.com/rancher/wrangler/pkg/schemas/validation"
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
     	"github.com/stretchr/testify/suite"
     )
     
    @@ -360,3 +366,127 @@ func (p *ServerSuite) TestServer_CustomAPIUIResponseWriter() {
     	assert.NotNil(p.T(), w.JSURL)
     	assert.NotNil(p.T(), w.APIUIVersion)
     }
    +
    +func TestServeHTMLEscaping(t *testing.T) {
    +	const (
    +		defaultJS         = "cattle.io"
    +		defaultCSS        = "cattle.io"
    +		defaultAPIVersion = "v1/apps.daemonsets.0.0"
    +		xss               = "<script>alert('xss')</script>"
    +		alphaNumeric      = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    +		badChars          = `~!@#$%^&*()_+-=[]\{}|;':",./<>?`
    +	)
    +	xssUrl := url.URL{RawPath: xss}
    +
    +	var escapedBadChars strings.Builder
    +	for _, r := range badChars {
    +		escapedBadChars.WriteString(fmt.Sprintf("&#x%X;", r))
    +	}
    +
    +	t.Parallel()
    +	tests := []struct {
    +		name             string
    +		CSSURL           string
    +		JSURL            string
    +		APIUIVersion     string
    +		URL              string
    +		desiredContent   string
    +		undesiredContent string
    +	}{
    +		{
    +			name:           "base case no xss",
    +			CSSURL:         defaultCSS,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: "https://cattle.io/v1/apps.daemonsets",
    +		},
    +		{
    +			name:           "JSS alpha-numeric",
    +			CSSURL:         defaultCSS,
    +			JSURL:          alphaNumeric,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "JSS escaped non alpha-numeric",
    +			CSSURL:           defaultCSS,
    +			JSURL:            badChars,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "CSS alpha-numeric",
    +			CSSURL:         alphaNumeric,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "CSS escaped non alpha-numeric",
    +			CSSURL:           badChars,
    +			JSURL:            defaultJS,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "api version alpha-numeric",
    +			APIUIVersion:   alphaNumeric,
    +			URL:            "https://cattle.io/v3",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "api version escaped non alpha-numeric",
    +			APIUIVersion:     badChars,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:             "Link XSS",
    +			URL:              "https://cattle.io/v1/apps.daemonsets" + xss,
    +			undesiredContent: xss,
    +			desiredContent:   xssUrl.String(),
    +		},
    +	}
    +	for _, test := range tests {
    +		tt := test
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			respStr, err := sendTestRequest(tt.URL, tt.CSSURL, tt.JSURL, tt.APIUIVersion)
    +			require.NoError(t, err, "failed to create server")
    +			require.Contains(t, respStr, tt.desiredContent, "expected content missing from server response")
    +			if tt.undesiredContent != "" {
    +				require.NotContains(t, respStr, tt.undesiredContent, "unexpected content found in server response")
    +			}
    +		})
    +	}
    +}
    +
    +func sendTestRequest(url, cssURL, jssURL, apiUIVersion string) (string, error) {
    +	resp := httptest.NewRecorder()
    +	req := httptest.NewRequest(http.MethodGet, url, nil)
    +	// These header values are needed to get an HTML return document
    +	req.Header.Set("Accept", "*/*")
    +	req.Header.Set("User-agent", "Mozilla")
    +	srv := DefaultAPIServer()
    +	srv.CustomAPIUIResponseWriter(stringGetter(cssURL), stringGetter(jssURL), stringGetter(apiUIVersion))
    +	srv.Schemas = builtin.Schemas
    +	apiOp := &types.APIRequest{
    +		Request:  req,
    +		Response: resp,
    +		Type:     "schema",
    +	}
    +	srv.Handle(apiOp)
    +	return resp.Body.String(), nil
    +}
    +
    +func stringGetter(val string) writer.StringGetter {
    +	return func() string { return val }
    +}
    
  • pkg/urlbuilder/base.go+5 4 modified
    @@ -2,16 +2,17 @@ package urlbuilder
     
     import (
     	"bytes"
    -	"fmt"
     	"net/http"
     	"net/url"
     	"strings"
     )
     
     func ParseRequestURL(r *http.Request) string {
    -	scheme := GetScheme(r)
    -	host := GetHost(r, scheme)
    -	return fmt.Sprintf("%s://%s%s%s", scheme, host, r.Header.Get(PrefixHeader), r.URL.Path)
    +	var parsedURL url.URL
    +	parsedURL.Scheme = GetScheme(r)
    +	parsedURL.Host = GetHost(r, parsedURL.Scheme)
    +	parsedURL = *parsedURL.JoinPath(r.Header.Get(PrefixHeader), r.URL.Path)
    +	return parsedURL.String()
     }
     
     func GetHost(r *http.Request, scheme string) string {
    
  • pkg/writer/html.go+29 1 modified
    @@ -2,6 +2,7 @@ package writer
     
     import (
     	"encoding/json"
    +	"fmt"
     	"strings"
     
     	"github.com/rancher/apiserver/pkg/types"
    @@ -10,7 +11,7 @@ import (
     const (
     	JSURL          = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.js"
     	CSSURL         = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.css"
    -	DefaultVersion = "1.1.10"
    +	DefaultVersion = "1.1.11"
     )
     
     var (
    @@ -71,6 +72,11 @@ func (h *HTMLResponseWriter) write(apiOp *types.APIRequest, code int, obj interf
     		jsurl = strings.Replace(JSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     		cssurl = strings.Replace(CSSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     	}
    +
    +	// jsurl and cssurl are added to the document as attributes not entities which requires special encoding.
    +	jsurl, _ = encodeAttribute(jsurl)
    +	cssurl, _ = encodeAttribute(cssurl)
    +
     	headerString = strings.Replace(headerString, "%JSURL%", jsurl, 1)
     	headerString = strings.Replace(headerString, "%CSSURL%", cssurl, 1)
     
    @@ -89,3 +95,25 @@ func jsonEncodeURL(str string) string {
     	data, _ := json.Marshal(str)
     	return string(data)
     }
    +
    +// encodeAttribute encodes all characters with the HTML Entity &#xHH; format, including spaces, where HH represents the hexadecimal value of the character in Unicode.
    +// For example, A becomes &#x41;. All alphanumeric characters (letters A to Z, a to z, and digits 0 to 9) remain unencoded.
    +// more info: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-rules-summary
    +func encodeAttribute(raw string) (string, error) {
    +	var builder strings.Builder
    +	for _, r := range raw {
    +		if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || ('0' <= r && r <= '9') {
    +			_, err := builder.WriteRune(r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		} else {
    +			// encode non-alphanumeric rune to hex.
    +			_, err := fmt.Fprintf(&builder, "&#x%X;", r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		}
    +	}
    +	return builder.String(), nil
    +}
    
4df268e250f6

[2.6] Fixes (#55)

https://github.com/rancher/apiserverPeter MatseykanetsFeb 5, 2024via ghsa
3 files changed · +173 5
  • pkg/server/server_test.go+139 0 added
    @@ -0,0 +1,139 @@
    +package server
    +
    +import (
    +	"fmt"
    +	"net/http"
    +	"net/http/httptest"
    +	"net/url"
    +	"strings"
    +	"testing"
    +
    +	"github.com/rancher/apiserver/pkg/builtin"
    +	"github.com/rancher/apiserver/pkg/types"
    +	"github.com/rancher/apiserver/pkg/writer"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func TestServeHTMLEscaping(t *testing.T) {
    +	const (
    +		defaultJS         = "cattle.io"
    +		defaultCSS        = "cattle.io"
    +		defaultAPIVersion = "v1/apps.daemonsets.0.0"
    +		xss               = "<script>alert('xss')</script>"
    +		alphaNumeric      = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    +		badChars          = `~!@#$%^&*()_+-=[]\{}|;':",./<>?`
    +	)
    +	xssUrl := url.URL{RawPath: xss}
    +
    +	var escapedBadChars strings.Builder
    +	for _, r := range badChars {
    +		escapedBadChars.WriteString(fmt.Sprintf("&#x%X;", r))
    +	}
    +
    +	t.Parallel()
    +	tests := []struct {
    +		name             string
    +		CSSURL           string
    +		JSURL            string
    +		APIUIVersion     string
    +		URL              string
    +		desiredContent   string
    +		undesiredContent string
    +	}{
    +		{
    +			name:           "base case no xss",
    +			CSSURL:         defaultCSS,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: "https://cattle.io/v1/apps.daemonsets",
    +		},
    +		{
    +			name:           "JSS alpha-numeric",
    +			CSSURL:         defaultCSS,
    +			JSURL:          alphaNumeric,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "JSS escaped non alpha-numeric",
    +			CSSURL:           defaultCSS,
    +			JSURL:            badChars,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "CSS alpha-numeric",
    +			CSSURL:         alphaNumeric,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "CSS escaped non alpha-numeric",
    +			CSSURL:           badChars,
    +			JSURL:            defaultJS,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "api version alpha-numeric",
    +			APIUIVersion:   alphaNumeric,
    +			URL:            "https://cattle.io/v3",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "api version escaped non alpha-numeric",
    +			APIUIVersion:     badChars,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:             "Link XSS",
    +			URL:              "https://cattle.io/v1/apps.daemonsets" + xss,
    +			undesiredContent: xss,
    +			desiredContent:   xssUrl.String(),
    +		},
    +	}
    +	for _, test := range tests {
    +		tt := test
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			respStr, err := sendTestRequest(tt.URL, tt.CSSURL, tt.JSURL, tt.APIUIVersion)
    +			require.NoError(t, err, "failed to create server")
    +			require.Contains(t, respStr, tt.desiredContent, "expected content missing from server response")
    +			if tt.undesiredContent != "" {
    +				require.NotContains(t, respStr, tt.undesiredContent, "unexpected content found in server response")
    +			}
    +		})
    +	}
    +}
    +
    +func sendTestRequest(url, cssURL, jssURL, apiUIVersion string) (string, error) {
    +	resp := httptest.NewRecorder()
    +	req := httptest.NewRequest(http.MethodGet, url, nil)
    +	// These header values are needed to get an HTML return document
    +	req.Header.Set("Accept", "*/*")
    +	req.Header.Set("User-agent", "Mozilla")
    +	srv := DefaultAPIServer()
    +	srv.CustomAPIUIResponseWriter(stringGetter(cssURL), stringGetter(jssURL), stringGetter(apiUIVersion))
    +	srv.Schemas = builtin.Schemas
    +	apiOp := &types.APIRequest{
    +		Request:  req,
    +		Response: resp,
    +		Type:     "schema",
    +	}
    +	srv.Handle(apiOp)
    +	return resp.Body.String(), nil
    +}
    +
    +func stringGetter(val string) writer.StringGetter {
    +	return func() string { return val }
    +}
    
  • pkg/urlbuilder/base.go+5 4 modified
    @@ -2,16 +2,17 @@ package urlbuilder
     
     import (
     	"bytes"
    -	"fmt"
     	"net/http"
     	"net/url"
     	"strings"
     )
     
     func ParseRequestURL(r *http.Request) string {
    -	scheme := GetScheme(r)
    -	host := GetHost(r, scheme)
    -	return fmt.Sprintf("%s://%s%s%s", scheme, host, r.Header.Get(PrefixHeader), r.URL.Path)
    +	var parsedURL url.URL
    +	parsedURL.Scheme = GetScheme(r)
    +	parsedURL.Host = GetHost(r, parsedURL.Scheme)
    +	parsedURL = *parsedURL.JoinPath(r.Header.Get(PrefixHeader), r.URL.Path)
    +	return parsedURL.String()
     }
     
     func GetHost(r *http.Request, scheme string) string {
    
  • pkg/writer/html.go+29 1 modified
    @@ -2,6 +2,7 @@ package writer
     
     import (
     	"encoding/json"
    +	"fmt"
     	"strings"
     
     	"github.com/rancher/apiserver/pkg/types"
    @@ -10,7 +11,7 @@ import (
     const (
     	JSURL          = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.js"
     	CSSURL         = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.css"
    -	DefaultVersion = "1.1.10"
    +	DefaultVersion = "1.1.11"
     )
     
     var (
    @@ -71,6 +72,11 @@ func (h *HTMLResponseWriter) write(apiOp *types.APIRequest, code int, obj interf
     		jsurl = strings.Replace(JSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     		cssurl = strings.Replace(CSSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     	}
    +
    +	// jsurl and cssurl are added to the document as attributes not entities which requires special encoding.
    +	jsurl, _ = encodeAttribute(jsurl)
    +	cssurl, _ = encodeAttribute(cssurl)
    +
     	headerString = strings.Replace(headerString, "%JSURL%", jsurl, 1)
     	headerString = strings.Replace(headerString, "%CSSURL%", cssurl, 1)
     
    @@ -89,3 +95,25 @@ func jsonEncodeURL(str string) string {
     	data, _ := json.Marshal(str)
     	return string(data)
     }
    +
    +// encodeAttribute encodes all characters with the HTML Entity &#xHH; format, including spaces, where HH represents the hexadecimal value of the character in Unicode.
    +// For example, A becomes &#x41;. All alphanumeric characters (letters A to Z, a to z, and digits 0 to 9) remain unencoded.
    +// more info: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-rules-summary
    +func encodeAttribute(raw string) (string, error) {
    +	var builder strings.Builder
    +	for _, r := range raw {
    +		if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || ('0' <= r && r <= '9') {
    +			_, err := builder.WriteRune(r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		} else {
    +			// encode non-alphanumeric rune to hex.
    +			_, err := fmt.Fprintf(&builder, "&#x%X;", r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		}
    +	}
    +	return builder.String(), nil
    +}
    
97a10a30200c

[2.7] Fixes (#54)

https://github.com/rancher/apiserverPeter MatseykanetsFeb 5, 2024via ghsa
3 files changed · +164 5
  • pkg/server/server_test.go+130 0 modified
    @@ -2,18 +2,24 @@ package server
     
     import (
     	"errors"
    +	"fmt"
     	"net/http"
    +	"net/http/httptest"
    +	"net/url"
    +	"strings"
     	"testing"
     
     	"github.com/golang/mock/gomock"
     	"github.com/rancher/apiserver/pkg/apierror"
    +	"github.com/rancher/apiserver/pkg/builtin"
     	"github.com/rancher/apiserver/pkg/fakes"
     	"github.com/rancher/apiserver/pkg/parse"
     	"github.com/rancher/apiserver/pkg/types"
     	"github.com/rancher/apiserver/pkg/writer"
     	"github.com/rancher/wrangler/pkg/schemas"
     	"github.com/rancher/wrangler/pkg/schemas/validation"
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
     	"github.com/stretchr/testify/suite"
     )
     
    @@ -360,3 +366,127 @@ func (p *ServerSuite) TestServer_CustomAPIUIResponseWriter() {
     	assert.NotNil(p.T(), w.JSURL)
     	assert.NotNil(p.T(), w.APIUIVersion)
     }
    +
    +func TestServeHTMLEscaping(t *testing.T) {
    +	const (
    +		defaultJS         = "cattle.io"
    +		defaultCSS        = "cattle.io"
    +		defaultAPIVersion = "v1/apps.daemonsets.0.0"
    +		xss               = "<script>alert('xss')</script>"
    +		alphaNumeric      = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    +		badChars          = `~!@#$%^&*()_+-=[]\{}|;':",./<>?`
    +	)
    +	xssUrl := url.URL{RawPath: xss}
    +
    +	var escapedBadChars strings.Builder
    +	for _, r := range badChars {
    +		escapedBadChars.WriteString(fmt.Sprintf("&#x%X;", r))
    +	}
    +
    +	t.Parallel()
    +	tests := []struct {
    +		name             string
    +		CSSURL           string
    +		JSURL            string
    +		APIUIVersion     string
    +		URL              string
    +		desiredContent   string
    +		undesiredContent string
    +	}{
    +		{
    +			name:           "base case no xss",
    +			CSSURL:         defaultCSS,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: "https://cattle.io/v1/apps.daemonsets",
    +		},
    +		{
    +			name:           "JSS alpha-numeric",
    +			CSSURL:         defaultCSS,
    +			JSURL:          alphaNumeric,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "JSS escaped non alpha-numeric",
    +			CSSURL:           defaultCSS,
    +			JSURL:            badChars,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "CSS alpha-numeric",
    +			CSSURL:         alphaNumeric,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "CSS escaped non alpha-numeric",
    +			CSSURL:           badChars,
    +			JSURL:            defaultJS,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "api version alpha-numeric",
    +			APIUIVersion:   alphaNumeric,
    +			URL:            "https://cattle.io/v3",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "api version escaped non alpha-numeric",
    +			APIUIVersion:     badChars,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:             "Link XSS",
    +			URL:              "https://cattle.io/v1/apps.daemonsets" + xss,
    +			undesiredContent: xss,
    +			desiredContent:   xssUrl.String(),
    +		},
    +	}
    +	for _, test := range tests {
    +		tt := test
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			respStr, err := sendTestRequest(tt.URL, tt.CSSURL, tt.JSURL, tt.APIUIVersion)
    +			require.NoError(t, err, "failed to create server")
    +			require.Contains(t, respStr, tt.desiredContent, "expected content missing from server response")
    +			if tt.undesiredContent != "" {
    +				require.NotContains(t, respStr, tt.undesiredContent, "unexpected content found in server response")
    +			}
    +		})
    +	}
    +}
    +
    +func sendTestRequest(url, cssURL, jssURL, apiUIVersion string) (string, error) {
    +	resp := httptest.NewRecorder()
    +	req := httptest.NewRequest(http.MethodGet, url, nil)
    +	// These header values are needed to get an HTML return document
    +	req.Header.Set("Accept", "*/*")
    +	req.Header.Set("User-agent", "Mozilla")
    +	srv := DefaultAPIServer()
    +	srv.CustomAPIUIResponseWriter(stringGetter(cssURL), stringGetter(jssURL), stringGetter(apiUIVersion))
    +	srv.Schemas = builtin.Schemas
    +	apiOp := &types.APIRequest{
    +		Request:  req,
    +		Response: resp,
    +		Type:     "schema",
    +	}
    +	srv.Handle(apiOp)
    +	return resp.Body.String(), nil
    +}
    +
    +func stringGetter(val string) writer.StringGetter {
    +	return func() string { return val }
    +}
    
  • pkg/urlbuilder/base.go+5 4 modified
    @@ -2,16 +2,17 @@ package urlbuilder
     
     import (
     	"bytes"
    -	"fmt"
     	"net/http"
     	"net/url"
     	"strings"
     )
     
     func ParseRequestURL(r *http.Request) string {
    -	scheme := GetScheme(r)
    -	host := GetHost(r, scheme)
    -	return fmt.Sprintf("%s://%s%s%s", scheme, host, r.Header.Get(PrefixHeader), r.URL.Path)
    +	var parsedURL url.URL
    +	parsedURL.Scheme = GetScheme(r)
    +	parsedURL.Host = GetHost(r, parsedURL.Scheme)
    +	parsedURL = *parsedURL.JoinPath(r.Header.Get(PrefixHeader), r.URL.Path)
    +	return parsedURL.String()
     }
     
     func GetHost(r *http.Request, scheme string) string {
    
  • pkg/writer/html.go+29 1 modified
    @@ -2,6 +2,7 @@ package writer
     
     import (
     	"encoding/json"
    +	"fmt"
     	"strings"
     
     	"github.com/rancher/apiserver/pkg/types"
    @@ -10,7 +11,7 @@ import (
     const (
     	JSURL          = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.js"
     	CSSURL         = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.css"
    -	DefaultVersion = "1.1.10"
    +	DefaultVersion = "1.1.11"
     )
     
     var (
    @@ -71,6 +72,11 @@ func (h *HTMLResponseWriter) write(apiOp *types.APIRequest, code int, obj interf
     		jsurl = strings.Replace(JSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     		cssurl = strings.Replace(CSSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     	}
    +
    +	// jsurl and cssurl are added to the document as attributes not entities which requires special encoding.
    +	jsurl, _ = encodeAttribute(jsurl)
    +	cssurl, _ = encodeAttribute(cssurl)
    +
     	headerString = strings.Replace(headerString, "%JSURL%", jsurl, 1)
     	headerString = strings.Replace(headerString, "%CSSURL%", cssurl, 1)
     
    @@ -89,3 +95,25 @@ func jsonEncodeURL(str string) string {
     	data, _ := json.Marshal(str)
     	return string(data)
     }
    +
    +// encodeAttribute encodes all characters with the HTML Entity &#xHH; format, including spaces, where HH represents the hexadecimal value of the character in Unicode.
    +// For example, A becomes &#x41;. All alphanumeric characters (letters A to Z, a to z, and digits 0 to 9) remain unencoded.
    +// more info: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-rules-summary
    +func encodeAttribute(raw string) (string, error) {
    +	var builder strings.Builder
    +	for _, r := range raw {
    +		if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || ('0' <= r && r <= '9') {
    +			_, err := builder.WriteRune(r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		} else {
    +			// encode non-alphanumeric rune to hex.
    +			_, err := fmt.Fprintf(&builder, "&#x%X;", r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		}
    +	}
    +	return builder.String(), nil
    +}
    
a3b9e3721c1b

[2.8] Fixes (#53)

https://github.com/rancher/apiserverPeter MatseykanetsFeb 5, 2024via ghsa
3 files changed · +164 5
  • pkg/server/server_test.go+130 0 modified
    @@ -2,18 +2,24 @@ package server
     
     import (
     	"errors"
    +	"fmt"
     	"net/http"
    +	"net/http/httptest"
    +	"net/url"
    +	"strings"
     	"testing"
     
     	"github.com/golang/mock/gomock"
     	"github.com/rancher/apiserver/pkg/apierror"
    +	"github.com/rancher/apiserver/pkg/builtin"
     	"github.com/rancher/apiserver/pkg/fakes"
     	"github.com/rancher/apiserver/pkg/parse"
     	"github.com/rancher/apiserver/pkg/types"
     	"github.com/rancher/apiserver/pkg/writer"
     	"github.com/rancher/wrangler/pkg/schemas"
     	"github.com/rancher/wrangler/pkg/schemas/validation"
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
     	"github.com/stretchr/testify/suite"
     )
     
    @@ -360,3 +366,127 @@ func (p *ServerSuite) TestServer_CustomAPIUIResponseWriter() {
     	assert.NotNil(p.T(), w.JSURL)
     	assert.NotNil(p.T(), w.APIUIVersion)
     }
    +
    +func TestServeHTMLEscaping(t *testing.T) {
    +	const (
    +		defaultJS         = "cattle.io"
    +		defaultCSS        = "cattle.io"
    +		defaultAPIVersion = "v1/apps.daemonsets.0.0"
    +		xss               = "<script>alert('xss')</script>"
    +		alphaNumeric      = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    +		badChars          = `~!@#$%^&*()_+-=[]\{}|;':",./<>?`
    +	)
    +	xssUrl := url.URL{RawPath: xss}
    +
    +	var escapedBadChars strings.Builder
    +	for _, r := range badChars {
    +		escapedBadChars.WriteString(fmt.Sprintf("&#x%X;", r))
    +	}
    +
    +	t.Parallel()
    +	tests := []struct {
    +		name             string
    +		CSSURL           string
    +		JSURL            string
    +		APIUIVersion     string
    +		URL              string
    +		desiredContent   string
    +		undesiredContent string
    +	}{
    +		{
    +			name:           "base case no xss",
    +			CSSURL:         defaultCSS,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: "https://cattle.io/v1/apps.daemonsets",
    +		},
    +		{
    +			name:           "JSS alpha-numeric",
    +			CSSURL:         defaultCSS,
    +			JSURL:          alphaNumeric,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "JSS escaped non alpha-numeric",
    +			CSSURL:           defaultCSS,
    +			JSURL:            badChars,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "CSS alpha-numeric",
    +			CSSURL:         alphaNumeric,
    +			JSURL:          defaultJS,
    +			APIUIVersion:   defaultAPIVersion,
    +			URL:            "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "CSS escaped non alpha-numeric",
    +			CSSURL:           badChars,
    +			JSURL:            defaultJS,
    +			APIUIVersion:     defaultAPIVersion,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:           "api version alpha-numeric",
    +			APIUIVersion:   alphaNumeric,
    +			URL:            "https://cattle.io/v3",
    +			desiredContent: alphaNumeric,
    +		},
    +		{
    +			name:             "api version escaped non alpha-numeric",
    +			APIUIVersion:     badChars,
    +			URL:              "https://cattle.io/v1/apps.daemonsets",
    +			desiredContent:   escapedBadChars.String(),
    +			undesiredContent: badChars,
    +		},
    +		{
    +			name:             "Link XSS",
    +			URL:              "https://cattle.io/v1/apps.daemonsets" + xss,
    +			undesiredContent: xss,
    +			desiredContent:   xssUrl.String(),
    +		},
    +	}
    +	for _, test := range tests {
    +		tt := test
    +		t.Run(tt.name, func(t *testing.T) {
    +			t.Parallel()
    +			respStr, err := sendTestRequest(tt.URL, tt.CSSURL, tt.JSURL, tt.APIUIVersion)
    +			require.NoError(t, err, "failed to create server")
    +			require.Contains(t, respStr, tt.desiredContent, "expected content missing from server response")
    +			if tt.undesiredContent != "" {
    +				require.NotContains(t, respStr, tt.undesiredContent, "unexpected content found in server response")
    +			}
    +		})
    +	}
    +}
    +
    +func sendTestRequest(url, cssURL, jssURL, apiUIVersion string) (string, error) {
    +	resp := httptest.NewRecorder()
    +	req := httptest.NewRequest(http.MethodGet, url, nil)
    +	// These header values are needed to get an HTML return document
    +	req.Header.Set("Accept", "*/*")
    +	req.Header.Set("User-agent", "Mozilla")
    +	srv := DefaultAPIServer()
    +	srv.CustomAPIUIResponseWriter(stringGetter(cssURL), stringGetter(jssURL), stringGetter(apiUIVersion))
    +	srv.Schemas = builtin.Schemas
    +	apiOp := &types.APIRequest{
    +		Request:  req,
    +		Response: resp,
    +		Type:     "schema",
    +	}
    +	srv.Handle(apiOp)
    +	return resp.Body.String(), nil
    +}
    +
    +func stringGetter(val string) writer.StringGetter {
    +	return func() string { return val }
    +}
    
  • pkg/urlbuilder/base.go+5 4 modified
    @@ -2,16 +2,17 @@ package urlbuilder
     
     import (
     	"bytes"
    -	"fmt"
     	"net/http"
     	"net/url"
     	"strings"
     )
     
     func ParseRequestURL(r *http.Request) string {
    -	scheme := GetScheme(r)
    -	host := GetHost(r, scheme)
    -	return fmt.Sprintf("%s://%s%s%s", scheme, host, r.Header.Get(PrefixHeader), r.URL.Path)
    +	var parsedURL url.URL
    +	parsedURL.Scheme = GetScheme(r)
    +	parsedURL.Host = GetHost(r, parsedURL.Scheme)
    +	parsedURL = *parsedURL.JoinPath(r.Header.Get(PrefixHeader), r.URL.Path)
    +	return parsedURL.String()
     }
     
     func GetHost(r *http.Request, scheme string) string {
    
  • pkg/writer/html.go+29 1 modified
    @@ -2,6 +2,7 @@ package writer
     
     import (
     	"encoding/json"
    +	"fmt"
     	"strings"
     
     	"github.com/rancher/apiserver/pkg/types"
    @@ -10,7 +11,7 @@ import (
     const (
     	JSURL          = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.js"
     	CSSURL         = "https://releases.rancher.com/api-ui/%API_UI_VERSION%/ui.min.css"
    -	DefaultVersion = "1.1.10"
    +	DefaultVersion = "1.1.11"
     )
     
     var (
    @@ -71,6 +72,11 @@ func (h *HTMLResponseWriter) write(apiOp *types.APIRequest, code int, obj interf
     		jsurl = strings.Replace(JSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     		cssurl = strings.Replace(CSSURL, "%API_UI_VERSION%", DefaultVersion, 1)
     	}
    +
    +	// jsurl and cssurl are added to the document as attributes not entities which requires special encoding.
    +	jsurl, _ = encodeAttribute(jsurl)
    +	cssurl, _ = encodeAttribute(cssurl)
    +
     	headerString = strings.Replace(headerString, "%JSURL%", jsurl, 1)
     	headerString = strings.Replace(headerString, "%CSSURL%", cssurl, 1)
     
    @@ -89,3 +95,25 @@ func jsonEncodeURL(str string) string {
     	data, _ := json.Marshal(str)
     	return string(data)
     }
    +
    +// encodeAttribute encodes all characters with the HTML Entity &#xHH; format, including spaces, where HH represents the hexadecimal value of the character in Unicode.
    +// For example, A becomes &#x41;. All alphanumeric characters (letters A to Z, a to z, and digits 0 to 9) remain unencoded.
    +// more info: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-rules-summary
    +func encodeAttribute(raw string) (string, error) {
    +	var builder strings.Builder
    +	for _, r := range raw {
    +		if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || ('0' <= r && r <= '9') {
    +			_, err := builder.WriteRune(r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		} else {
    +			// encode non-alphanumeric rune to hex.
    +			_, err := fmt.Fprintf(&builder, "&#x%X;", r)
    +			if err != nil {
    +				return "", fmt.Errorf("failed to write: %w", err)
    +			}
    +		}
    +	}
    +	return builder.String(), nil
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

10

News mentions

0

No linked articles in our index yet.