A TarSlip vulnerability exists in the deepjavalibrary/djl, affecting version 0.26.0 and fixed in version 0.27.0. This vulnerability allows an attacker to manipulate file paths within tar archives to overwrite arbitrary files on the target system. Exploitation of this vulnerability could lead to remote code execution, privilege escalation, data theft or manipulation, and denial of service. The vulnerability is due to improper validation of file paths during the extraction of tar files, as demonstrated in multiple occurrences within the library's codebase, including but not limited to the files_util.py and extract_imagenet.py scripts.

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

def download_and_extract(url, path):
    """Download and extract a tar file.

    :param url: The url of the tar file.
    :param path: The path to the file to download to.
    """
    if not os.path.exists(path):
        os.makedirs(path)
    if not os.listdir(path):
        with tmpdir() as tmp:
            tmp_file = os.path.join(tmp, "tar_file")
            if url.startswith("s3://"):
                s3_download(url, tmp_file)
                with tarfile.open(name=tmp_file, mode="r:gz") as t:
                    t.extractall(path=path)
            elif url.startswith("http://") or url.startswith("https://"):
                with urlopen(url) as response, open(tmp_file, 'wb') as f:
                    shutil.copyfileobj(response, f)
                with tarfile.open(name=tmp_file, mode="r:gz") as t:
                    t.extractall(path=path)

def download_and_extract(url, path):
    """Download and extract a tar file.

    :param url: The url of the tar file.
    :param path: The path to the file to download to.
    """
    def is_within_directory(directory, target):
        abs_directory = os.path.abspath(directory)
        abs_target = os.path.abspath(target)
        prefix = os.path.commonprefix([abs_directory, abs_target])
        return prefix == abs_directory

    def safe_extract(tar, path=".", members=None, *, numeric_owner=False):
        for member in tar.getmembers():
            member_path = os.path.join(path, member.name)
            if not is_within_directory(path, member_path):
                raise Exception("Attempted Path Traversal in Tar File")

        tar.extractall(path, members, numeric_owner=numeric_owner)

    if not os.path.exists(path):
        os.makedirs(path)
    if not os.listdir(path):
        with tmpdir() as tmp:
            tmp_file = os.path.join(tmp, "tar_file")
            if url.startswith("s3://"):
                s3_download(url, tmp_file)
                with tarfile.open(name=tmp_file, mode="r:gz") as t:
                    safe_extract(t, path=path)
            elif url.startswith("http://") or url.startswith("https://"):
                with urlopen(url) as response, open(tmp_file, 'wb') as f:
                    shutil.copyfileobj(response, f)
                with tarfile.open(name=tmp_file, mode="r:gz") as t:
                    safe_extract(t, path=path)

--- a/extensions/spark/setup/djl_spark/util/files_util.py
+++ b/extensions/spark/setup/djl_spark/util/files_util.py
@@ -4,6 +4,20 @@
     :param url: The url of the tar file.
     :param path: The path to the file to download to.
     """
+    def is_within_directory(directory, target):
+        abs_directory = os.path.abspath(directory)
+        abs_target = os.path.abspath(target)
+        prefix = os.path.commonprefix([abs_directory, abs_target])
+        return prefix == abs_directory
+
+    def safe_extract(tar, path=".", members=None, *, numeric_owner=False):
+        for member in tar.getmembers():
+            member_path = os.path.join(path, member.name)
+            if not is_within_directory(path, member_path):
+                raise Exception("Attempted Path Traversal in Tar File")
+
+        tar.extractall(path, members, numeric_owner=numeric_owner)
+
     if not os.path.exists(path):
         os.makedirs(path)
     if not os.listdir(path):
@@ -12,9 +26,9 @@
             if url.startswith("s3://"):
                 s3_download(url, tmp_file)
                 with tarfile.open(name=tmp_file, mode="r:gz") as t:
-                    t.extractall(path=path)
+                    safe_extract(t, path=path)
             elif url.startswith("http://") or url.startswith("https://"):
                 with urlopen(url) as response, open(tmp_file, 'wb') as f:
                     shutil.copyfileobj(response, f)
                 with tarfile.open(name=tmp_file, mode="r:gz") as t:
-                    t.extractall(path=path)
+                    safe_extract(t, path=path)


The package github.com/hoppscotch/proxyscotch before 1.0.0 are vulnerable to Server-side Request Forgery (SSRF) when interceptor mode is set to proxy. It occurs when an HTTP request is made by a backend server to an untrusted URL submitted by a user. It leads to a leakage of sensitive information from the server.

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

var (
	accessToken        string
	sessionFingerprint string
	allowedOrigins     []string
	bannedOutputs      []string
)

func Initialize(
	initialAccessToken string,
	proxyURL string,
	initialAllowedOrigins string,
	initialBannedOutputs string,
	onStatusChange statusChangeFunction,
	withSSL bool,
	finished chan bool,
) {
	if initialBannedOutputs != "" {
		bannedOutputs = strings.Split(initialBannedOutputs, ",")
	}
	allowedOrigins = strings.Split(initialAllowedOrigins, ",")
	accessToken = initialAccessToken
	sessionFingerprint = uuid.New().String()
	log.Println("Starting proxy server...")

	http.HandleFunc("/", proxyHandler)

	if !withSSL {
		go func() {
			httpServerError := http.ListenAndServe(proxyURL, nil)

			if httpServerError != nil {
				onStatusChange("An error occurred: "+httpServerError.Error(), false)
			}

			finished <- true
		}()

		onStatusChange("Listening on http://"+proxyURL+"/", true)
	} else {
		onStatusChange("Checking SSL certificate...", false)

		err := EnsurePrivateKeyInstalled()
		if err != nil {
			log.Println(err.Error())
			onStatusChange("An error occurred.", false)
		}

		go func() {
			httpServerError := http.ListenAndServeTLS(proxyURL, GetOrCreateDataPath()+"/cert.pem", GetOrCreateDataPath()+"/key.pem", nil)

			if httpServerError != nil {
				onStatusChange("An error occurred.", false)
			}
		}()

		onStatusChange("Listening on https://"+proxyURL+"/", true)
		log.Println("Proxy server listening on https://" + proxyURL + "/")
	}
}

func proxyHandler(response http.ResponseWriter, request *http.Request) {
	// We want to allow all types of requests to the proxy, though we only want to allow certain
	// origins.
	response.Header().Add("Access-Control-Allow-Headers", "*")
	if request.Method == "OPTIONS" {
		response.Header().Add("Access-Control-Allow-Origin", "*")
		response.WriteHeader(200)
		return
	}

	if request.Header.Get("Origin") == "" || !isAllowedOrigin(request.Header.Get("Origin")) {
		if strings.HasPrefix(request.Header.Get("Content-Type"), "application/json") {
			response.Header().Add("Access-Control-Allow-Headers", "*")
			response.Header().Add("Access-Control-Allow-Origin", "*")
			response.WriteHeader(200)
			_, _ = fmt.Fprintln(response, ErrorBodyProxyRequestFailed)
			return
		}

		// If it is not an allowed origin, redirect back to hoppscotch.io.
		response.Header().Add("Location", "https://hoppscotch.io/")
		response.WriteHeader(301)
		return
	} else {
		// Otherwise set the appropriate CORS polciy and continue.
		response.Header().Add("Access-Control-Allow-Origin", request.Header.Get("Origin"))
	}

	// For anything other than an POST request, we'll return an empty JSON object.
	response.Header().Add("Content-Type", "application/json; charset=utf-8")
	if request.Method != "POST" {
		_, _ = fmt.Fprintln(response, "{\"success\": true, \"data\":{\"sessionFingerprint\":\""+sessionFingerprint+"\", \"isProtected\":"+strconv.FormatBool(len(accessToken) > 0)+"}}")
		return
	}

	// Attempt to parse request body.
	var requestData Request
	isMultipart := strings.HasPrefix(request.Header.Get("content-type"), "multipart/form-data")
	var multipartRequestDataKey = request.Header.Get("multipart-part-key")
	if multipartRequestDataKey == "" {
		multipartRequestDataKey = "proxyRequestData"
	}
	if isMultipart {
		var err = request.ParseMultipartForm(maxMemory)
		if err != nil {
			log.Printf("Failed to parse request body: %v", err)
			_, _ = fmt.Fprintln(response, ErrorBodyInvalidRequest)
			return
		}
		r := request.MultipartForm.Value[multipartRequestDataKey]
		err = json.Unmarshal([]byte(r[0]), &requestData)
		if err != nil || len(requestData.Url) == 0 || len(requestData.Method) == 0 {
			// If the logged err is nil here, it means either the URL or method were not supplied
			// in the request data.
			log.Printf("Failed to parse request body: %v", err)
			_, _ = fmt.Fprintln(response, ErrorBodyInvalidRequest)
			return
		}
	} else {
		var err = json.NewDecoder(request.Body).Decode(&requestData)
		if err != nil || len(requestData.Url) == 0 || len(requestData.Method) == 0 {
			// If the logged err is nil here, it means either the URL or method were not supplied
			// in the request data.
			log.Printf("Failed to parse request body: %v", err)
			_, _ = fmt.Fprintln(response, ErrorBodyInvalidRequest)
			return
		}
	}

	if len(accessToken) > 0 && requestData.AccessToken != accessToken {
		log.Print("An unauthorized request was made.")
		_, _ = fmt.Fprintln(response, "{\"success\": false, \"data\":{\"message\":\"(Proxy Error) Unauthorized request; you may need to set your access token in Settings.\"}}")
		return
	}

	// Make the request
	var proxyRequest http.Request
	proxyRequest.Header = make(http.Header)
	proxyRequest.Method = requestData.Method
	proxyRequest.URL, _ = url.Parse(requestData.Url)

	var params = proxyRequest.URL.Query()

	for k, v := range requestData.Params {
		params.Set(k, v)
	}
	proxyRequest.URL.RawQuery = params.Encode()

	if len(requestData.Auth.Username) > 0 && len(requestData.Auth.Password) > 0 {
		proxyRequest.SetBasicAuth(requestData.Auth.Username, requestData.Auth.Password)
	}
	for k, v := range requestData.Headers {
		proxyRequest.Header.Set(k, v)
	}

	proxyRequest.Header.Set("User-Agent", "Proxyscotch/1.1")

	if isMultipart {
		body := &bytes.Buffer{}
		writer := multipart.NewWriter(body)
		for key := range request.MultipartForm.Value {
			if key == multipartRequestDataKey {
				continue
			}
			for _, val := range request.MultipartForm.Value[key] {
				// This usually never happens, mostly memory issue
				err := writer.WriteField(key, val)
				if err != nil {
					log.Printf("Failed to write multipart field key: %s error: %v", key, err)
					return
				}
			}
		}
		for fileKey := range request.MultipartForm.File {
			for _, val := range request.MultipartForm.File[fileKey] {
				f, err := val.Open()
				if err != nil {
					log.Printf("Failed to write multipart field: %s err: %v", fileKey, err)
					continue
				}
				field, _ := writer.CreatePart(val.Header)
				_, err = io.Copy(field, f)
				if err != nil {
					log.Printf("Failed to write multipart field: %s err: %v", fileKey, err)
				}
				// Close need not be handled, as go will clear temp file
				defer func(f multipart.File) {
					err := f.Close()
					if err != nil {
						log.Printf("Failed to close file")
					}
				}(f)
			}
		}
		err := writer.Close()
		if err != nil {
			log.Printf("Failed to write multipart content: %v", err)
			_, _ = fmt.Fprintf(response, ErrorBodyProxyRequestFailed)
			if err != nil {
				return
			}
			return
		}
		contentType := fmt.Sprintf("multipart/form-data; boundary=%v", writer.Boundary())
		proxyRequest.Header.Set("content-type", contentType)
		proxyRequest.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
		proxyRequest.Body.Close()
	} else if len(requestData.Data) > 0 {
		proxyRequest.Body = ioutil.NopCloser(strings.NewReader(requestData.Data))
		proxyRequest.Body.Close()
	}

	var client http.Client
	var proxyResponse *http.Response
	proxyResponse, err := client.Do(&proxyRequest)

	if err != nil {
		log.Print("Failed to write response body: ", err.Error())
		_, _ = fmt.Fprintln(response, ErrorBodyProxyRequestFailed)
		return
	}

	var responseData Response
	responseData.Success = true
	responseData.Status = proxyResponse.StatusCode
	responseData.StatusText = strings.Join(strings.Split(proxyResponse.Status, " ")[1:], " ")
	responseBytes, err := ioutil.ReadAll(proxyResponse.Body)
	responseData.Headers = headerToArray(proxyResponse.Header)

	if requestData.WantsBinary {
		for _, bannedOutput := range bannedOutputs {
			responseBytes = bytes.ReplaceAll(responseBytes, []byte(bannedOutput), []byte("[redacted]"))
		}

		// If using the new binary format, encode the response body.
		responseData.Data = base64.RawStdEncoding.EncodeToString(responseBytes)
		responseData.IsBinary = true
	} else {
		// Otherwise, simply return the old format.
		responseData.Data = string(responseBytes)

		for _, bannedOutput := range bannedOutputs {
			responseData.Data = strings.Replace(responseData.Data, bannedOutput, "[redacted]", -1)
		}
	}

	// Write the request body to the response.
	err = json.NewEncoder(response).Encode(responseData)

	// Return the response.
	if err != nil {
		log.Print("Failed to write response body: ", err.Error())
		_, _ = fmt.Fprintln(response, ErrorBodyProxyRequestFailed)
		return
	}
}

func main() {
	hostPtr := flag.String("host", "localhost:9159", "the hostname that the server should listen on.")
	tokenPtr := flag.String("token", "", "the Proxy Access Token used to restrict access to the server.")
	allowedOriginsPtr := flag.String("allowed-origins", "*", "a comma separated list of allowed origins.")
	bannedOutputsPtr := flag.String("banned-outputs", "", "a comma separated list of banned outputs.")
	flag.Parse()

	finished := make(chan bool)
	libproxy.Initialize(*tokenPtr, *hostPtr, *allowedOriginsPtr, *bannedOutputsPtr, onProxyStateChangeServer, false, finished)

	<-finished
}

var (
	accessToken        string
	sessionFingerprint string
	allowedOrigins     []string
	bannedOutputs      []string
	bannedDests        []string
)

func Initialize(
	initialAccessToken string,
	proxyURL string,
	initialAllowedOrigins string,
	initialBannedOutputs string,
	initialBannedDests string,
	onStatusChange statusChangeFunction,
	withSSL bool,
	finished chan bool,
) {
	if initialBannedOutputs != "" {
		bannedOutputs = strings.Split(initialBannedOutputs, ",")
	}
	if initialBannedDests != "" {
		bannedDests = strings.Split(initialBannedDests, ",")
	} else {
		bannedDests = []string{}
	}
	allowedOrigins = strings.Split(initialAllowedOrigins, ",")
	accessToken = initialAccessToken
	sessionFingerprint = uuid.New().String()
	log.Println("Starting proxy server...")

	http.HandleFunc("/", proxyHandler)

	if !withSSL {
		go func() {
			httpServerError := http.ListenAndServe(proxyURL, nil)

			if httpServerError != nil {
				onStatusChange("An error occurred: "+httpServerError.Error(), false)
			}

			finished <- true
		}()

		onStatusChange("Listening on http://"+proxyURL+"/", true)
	} else {
		onStatusChange("Checking SSL certificate...", false)

		err := EnsurePrivateKeyInstalled()
		if err != nil {
			log.Println(err.Error())
			onStatusChange("An error occurred.", false)
		}

		go func() {
			httpServerError := http.ListenAndServeTLS(proxyURL, GetOrCreateDataPath()+"/cert.pem", GetOrCreateDataPath()+"/key.pem", nil)

			if httpServerError != nil {
				onStatusChange("An error occurred.", false)
			}
		}()

		onStatusChange("Listening on https://"+proxyURL+"/", true)
		log.Println("Proxy server listening on https://" + proxyURL + "/")
	}
}

func proxyHandler(response http.ResponseWriter, request *http.Request) {
	// We want to allow all types of requests to the proxy, though we only want to allow certain
	// origins.
	response.Header().Add("Access-Control-Allow-Headers", "*")
	if request.Method == "OPTIONS" {
		response.Header().Add("Access-Control-Allow-Origin", "*")
		response.WriteHeader(200)
		return
	}

	if request.Header.Get("Origin") == "" || !isAllowedOrigin(request.Header.Get("Origin")) {
		if strings.HasPrefix(request.Header.Get("Content-Type"), "application/json") {
			response.Header().Add("Access-Control-Allow-Headers", "*")
			response.Header().Add("Access-Control-Allow-Origin", "*")
			response.WriteHeader(200)
			_, _ = fmt.Fprintln(response, ErrorBodyProxyRequestFailed)
			return
		}

		// If it is not an allowed origin, redirect back to hoppscotch.io.
		response.Header().Add("Location", "https://hoppscotch.io/")
		response.WriteHeader(301)
		return
	} else {
		// Otherwise set the appropriate CORS polciy and continue.
		response.Header().Add("Access-Control-Allow-Origin", request.Header.Get("Origin"))
	}

	// For anything other than an POST request, we'll return an empty JSON object.
	response.Header().Add("Content-Type", "application/json; charset=utf-8")
	if request.Method != "POST" {
		_, _ = fmt.Fprintln(response, "{\"success\": true, \"data\":{\"sessionFingerprint\":\""+sessionFingerprint+"\", \"isProtected\":"+strconv.FormatBool(len(accessToken) > 0)+"}}")
		return
	}

	// Attempt to parse request body.
	var requestData Request
	isMultipart := strings.HasPrefix(request.Header.Get("content-type"), "multipart/form-data")
	var multipartRequestDataKey = request.Header.Get("multipart-part-key")
	if multipartRequestDataKey == "" {
		multipartRequestDataKey = "proxyRequestData"
	}
	if isMultipart {
		var err = request.ParseMultipartForm(maxMemory)
		if err != nil {
			log.Printf("Failed to parse request body: %v", err)
			_, _ = fmt.Fprintln(response, ErrorBodyInvalidRequest)
			return
		}
		r := request.MultipartForm.Value[multipartRequestDataKey]
		err = json.Unmarshal([]byte(r[0]), &requestData)
		if err != nil || len(requestData.Url) == 0 || len(requestData.Method) == 0 {
			// If the logged err is nil here, it means either the URL or method were not supplied
			// in the request data.
			log.Printf("Failed to parse request body: %v", err)
			_, _ = fmt.Fprintln(response, ErrorBodyInvalidRequest)
			return
		}
	} else {
		var err = json.NewDecoder(request.Body).Decode(&requestData)
		if err != nil || len(requestData.Url) == 0 || len(requestData.Method) == 0 {
			// If the logged err is nil here, it means either the URL or method were not supplied
			// in the request data.
			log.Printf("Failed to parse request body: %v", err)
			_, _ = fmt.Fprintln(response, ErrorBodyInvalidRequest)
			return
		}
	}

	if len(accessToken) > 0 && requestData.AccessToken != accessToken {
		log.Print("An unauthorized request was made.")
		_, _ = fmt.Fprintln(response, "{\"success\": false, \"data\":{\"message\":\"(Proxy Error) Unauthorized request; you may need to set your access token in Settings.\"}}")
		return
	}

	// Make the request
	var proxyRequest http.Request
	proxyRequest.Header = make(http.Header)
	proxyRequest.Method = requestData.Method
	proxyRequest.URL, _ = url.Parse(requestData.Url)

	// Block requests to illegal destinations
	if !isAllowedDest(proxyRequest.URL.Hostname()) {
		log.Print("A request to a banned destination was made.")
		_, _ = fmt.Fprintln(response, "{\"success\": false, \"data\":{\"message\":\"(Proxy Error) Request cannot be to this destination.\"}}")
		return
	}

	var params = proxyRequest.URL.Query()

	for k, v := range requestData.Params {
		params.Set(k, v)
	}
	proxyRequest.URL.RawQuery = params.Encode()

	if len(requestData.Auth.Username) > 0 && len(requestData.Auth.Password) > 0 {
		proxyRequest.SetBasicAuth(requestData.Auth.Username, requestData.Auth.Password)
	}
	for k, v := range requestData.Headers {
		proxyRequest.Header.Set(k, v)
	}

	proxyRequest.Header.Set("User-Agent", "Proxyscotch/1.1")

	if isMultipart {
		body := &bytes.Buffer{}
		writer := multipart.NewWriter(body)
		for key := range request.MultipartForm.Value {
			if key == multipartRequestDataKey {
				continue
			}
			for _, val := range request.MultipartForm.Value[key] {
				// This usually never happens, mostly memory issue
				err := writer.WriteField(key, val)
				if err != nil {
					log.Printf("Failed to write multipart field key: %s error: %v", key, err)
					return
				}
			}
		}
		for fileKey := range request.MultipartForm.File {
			for _, val := range request.MultipartForm.File[fileKey] {
				f, err := val.Open()
				if err != nil {
					log.Printf("Failed to write multipart field: %s err: %v", fileKey, err)
					continue
				}
				field, _ := writer.CreatePart(val.Header)
				_, err = io.Copy(field, f)
				if err != nil {
					log.Printf("Failed to write multipart field: %s err: %v", fileKey, err)
				}
				// Close need not be handled, as go will clear temp file
				defer func(f multipart.File) {
					err := f.Close()
					if err != nil {
						log.Printf("Failed to close file")
					}
				}(f)
			}
		}
		err := writer.Close()
		if err != nil {
			log.Printf("Failed to write multipart content: %v", err)
			_, _ = fmt.Fprintf(response, ErrorBodyProxyRequestFailed)
			if err != nil {
				return
			}
			return
		}
		contentType := fmt.Sprintf("multipart/form-data; boundary=%v", writer.Boundary())
		proxyRequest.Header.Set("content-type", contentType)
		proxyRequest.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
		proxyRequest.Body.Close()
	} else if len(requestData.Data) > 0 {
		proxyRequest.Body = ioutil.NopCloser(strings.NewReader(requestData.Data))
		proxyRequest.Body.Close()
	}

	var client http.Client
	var proxyResponse *http.Response
	proxyResponse, err := client.Do(&proxyRequest)

	if err != nil {
		log.Print("Failed to write response body: ", err.Error())
		_, _ = fmt.Fprintln(response, ErrorBodyProxyRequestFailed)
		return
	}

	var responseData Response
	responseData.Success = true
	responseData.Status = proxyResponse.StatusCode
	responseData.StatusText = strings.Join(strings.Split(proxyResponse.Status, " ")[1:], " ")
	responseBytes, err := ioutil.ReadAll(proxyResponse.Body)
	responseData.Headers = headerToArray(proxyResponse.Header)

	if requestData.WantsBinary {
		for _, bannedOutput := range bannedOutputs {
			responseBytes = bytes.ReplaceAll(responseBytes, []byte(bannedOutput), []byte("[redacted]"))
		}

		// If using the new binary format, encode the response body.
		responseData.Data = base64.RawStdEncoding.EncodeToString(responseBytes)
		responseData.IsBinary = true
	} else {
		// Otherwise, simply return the old format.
		responseData.Data = string(responseBytes)

		for _, bannedOutput := range bannedOutputs {
			responseData.Data = strings.Replace(responseData.Data, bannedOutput, "[redacted]", -1)
		}
	}

	// Write the request body to the response.
	err = json.NewEncoder(response).Encode(responseData)

	// Return the response.
	if err != nil {
		log.Print("Failed to write response body: ", err.Error())
		_, _ = fmt.Fprintln(response, ErrorBodyProxyRequestFailed)
		return
	}
}

func main() {
	hostPtr := flag.String("host", "localhost:9159", "the hostname that the server should listen on.")
	tokenPtr := flag.String("token", "", "the Proxy Access Token used to restrict access to the server.")
	allowedOriginsPtr := flag.String("allowed-origins", "*", "a comma separated list of allowed origins.")
	bannedOutputsPtr := flag.String("banned-outputs", "", "a comma separated list of banned outputs.")
	bannedDestsPtr := flag.String("banned-dests", "", "a comma separated list of banned proxy destinations.")

	flag.Parse()

	finished := make(chan bool)
	libproxy.Initialize(*tokenPtr, *hostPtr, *allowedOriginsPtr, *bannedOutputsPtr, *bannedDestsPtr, onProxyStateChangeServer, false, finished)

	<-finished
}

func isAllowedDest(dest string) bool {
	for _, b := range bannedDests {
		if b == dest {
			return false
		}
	}

	return true
}

--- a/libproxy/proxy.go
+++ b/libproxy/proxy.go
@@ -3,4 +3,5 @@
 	sessionFingerprint string
 	allowedOrigins     []string
 	bannedOutputs      []string
+	bannedDests        []string
 )

--- a/libproxy/proxy.go
+++ b/libproxy/proxy.go
@@ -3,12 +3,18 @@
 	proxyURL string,
 	initialAllowedOrigins string,
 	initialBannedOutputs string,
+	initialBannedDests string,
 	onStatusChange statusChangeFunction,
 	withSSL bool,
 	finished chan bool,
 ) {
 	if initialBannedOutputs != "" {
 		bannedOutputs = strings.Split(initialBannedOutputs, ",")
+	}
+	if initialBannedDests != "" {
+		bannedDests = strings.Split(initialBannedDests, ",")
+	} else {
+		bannedDests = []string{}
 	}
 	allowedOrigins = strings.Split(initialAllowedOrigins, ",")
 	accessToken = initialAccessToken

--- a/libproxy/proxy.go
+++ b/libproxy/proxy.go
@@ -78,6 +78,13 @@
 	proxyRequest.Header = make(http.Header)
 	proxyRequest.Method = requestData.Method
 	proxyRequest.URL, _ = url.Parse(requestData.Url)
+
+	// Block requests to illegal destinations
+	if !isAllowedDest(proxyRequest.URL.Hostname()) {
+		log.Print("A request to a banned destination was made.")
+		_, _ = fmt.Fprintln(response, "{\"success\": false, \"data\":{\"message\":\"(Proxy Error) Request cannot be to this destination.\"}}")
+		return
+	}

 	var params = proxyRequest.URL.Query()

--- a/server/server.go
+++ b/server/server.go
@@ -3,10 +3,12 @@
 	tokenPtr := flag.String("token", "", "the Proxy Access Token used to restrict access to the server.")
 	allowedOriginsPtr := flag.String("allowed-origins", "*", "a comma separated list of allowed origins.")
 	bannedOutputsPtr := flag.String("banned-outputs", "", "a comma separated list of banned outputs.")
+	bannedDestsPtr := flag.String("banned-dests", "", "a comma separated list of banned proxy destinations.")
+
 	flag.Parse()

 	finished := make(chan bool)
-	libproxy.Initialize(*tokenPtr, *hostPtr, *allowedOriginsPtr, *bannedOutputsPtr, onProxyStateChangeServer, false, finished)
+	libproxy.Initialize(*tokenPtr, *hostPtr, *allowedOriginsPtr, *bannedOutputsPtr, *bannedDestsPtr, onProxyStateChangeServer, false, finished)

 	<-finished
 }

--- /dev/null
+++ b/server/server.go
@@ -0,0 +1,9 @@
+func isAllowedDest(dest string) bool {
+	for _, b := range bannedDests {
+		if b == dest {
+			return false
+		}
+	}
+
+	return true
+}


goshs is a SimpleHTTPServer written in Go. Starting in version 0.3.4 and prior to version 1.0.5, running goshs without arguments makes it possible for anyone to execute commands on the server. The function `dispatchReadPump` does not checks the option cli `-c`, thus allowing anyone to execute arbitrary command through the use of websockets. Version 1.0.5 fixes the issue.

Improper Access Control

The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.

func (fs *FileServer) Start(what string) {
	// Setup routing with gorilla/mux
	mux := NewCustomMux()

	addr := fs.SetupMux(mux, what)

	// construct and bind listener
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		logger.Fatalf("Error binding to listener '%s': %+v", addr, err)
	}
	defer func() {
		if err := listener.Close(); err != nil {
			logger.Errorf("error closing tcp listener: %+v", err)
		}
	}()

	// construct server
	server := http.Server{
		// Addr:              addr,
		Handler:           http.AllowQuerySemicolons(mux),
		ReadHeaderTimeout: 10 * time.Second, // Mitigate Slow Loris Attack
		ErrorLog:          log.New(io.Discard, "", 0),
		// Against good practice no timeouts here, otherwise big files would be terminated when downloaded
	}

	// init clipboard
	if !fs.NoClipboard {
		fs.Clipboard = clipboard.New()

		// init websocket hub
		fs.Hub = ws.NewHub(fs.Clipboard)
		go fs.Hub.Run()
	}

	// Print silent banner
	if fs.Silent {
		logger.Info("Serving in silent mode - no dir listing available at HTTP Listener")
	}

	// Print all embedded files as info to the console
	fs.PrintEmbeddedFiles()

	// Register webhook
	if fs.WebhookEnable {
		switch strings.ToLower(fs.WebhookProvider) {
		case "discord":
			fs.Webhook = &webhook.DiscordWebhook{
				URL:      fs.WebhookURL,
				Username: "goshs",
			}
		case "slack":
			fs.Webhook = &webhook.SlackWebhook{
				URL: fs.WebhookURL,
			}
		case "mattermost":
			fs.Webhook = &webhook.MattermostWebhook{
				URL:      fs.WebhookURL,
				Username: "goshs",
			}
		default:
			logger.Fatalf("Webhook provider '%s' not supported", fs.WebhookProvider)
		}
	}

	// Start listener
	fs.StartListener(server, what, listener)
}

func (c *Client) dispatchReadPump(packet Packet) {
	// Switch here over possible socket events and pull in handlers
	switch packet.Type {
	case "newEntry":
		var entry string
		if err := json.Unmarshal(packet.Content, &entry); err != nil {
			logger.Errorf("Error reading json packet: %+v", err)
		}
		if err := c.hub.cb.AddEntry(entry); err != nil {
			logger.Errorf("Error creating Clipboard entry: %+v", err)
		}
		c.refreshClipboard()

	case "delEntry":
		var id string
		if err := json.Unmarshal(packet.Content, &id); err != nil {
			logger.Errorf("Error reading json packet: %+v", err)
		}
		iid, err := strconv.Atoi(id)
		if err != nil {
			logger.Errorf("Error reading json packet: %+v", err)
		}
		if err := c.hub.cb.DeleteEntry(iid); err != nil {
			logger.Errorf("Error to delete Clipboard entry with id: %s: %+v", string(packet.Content), err)
		}
		c.refreshClipboard()

	case "clearClipboard":
		if err := c.hub.cb.ClearClipboard(); err != nil {
			logger.Errorf("Error clearing clipboard: %+v", err)
		}
		c.refreshClipboard()

	case "command":
		var command string
		if err := json.Unmarshal(packet.Content, &command); err != nil {
			logger.Errorf("Error reading json packet: %+v", err)
		}
		logger.Debugf("Command was: %+v", command)
		output, err := cli.RunCMD(command)
		if err != nil {
			logger.Errorf("Error running command: %+v", err)
		}
		logger.Debugf("Output: %+v", output)
		c.updateCLI(output)

	default:
		logger.Warnf("The event sent via websocket cannot be handeled: %+v", packet.Type)
	}

}

type Hub struct {
	// Registered clients.
	clients map[*Client]bool

	// Inbound messages from the clients.
	broadcast chan []byte

	// Register requests from the clients.
	register chan *Client

	// Unregister requests from clients.
	unregister chan *Client

	// Handle clipboard
	cb *clipboard.Clipboard
}

func NewHub(cb *clipboard.Clipboard) *Hub {
	return &Hub{
		broadcast:  make(chan []byte),
		register:   make(chan *Client),
		unregister: make(chan *Client),
		clients:    make(map[*Client]bool),
		cb:         cb,
	}
}

func (fs *FileServer) Start(what string) {
	// Setup routing with gorilla/mux
	mux := NewCustomMux()

	addr := fs.SetupMux(mux, what)

	// construct and bind listener
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		logger.Fatalf("Error binding to listener '%s': %+v", addr, err)
	}
	defer func() {
		if err := listener.Close(); err != nil {
			logger.Errorf("error closing tcp listener: %+v", err)
		}
	}()

	// construct server
	server := http.Server{
		// Addr:              addr,
		Handler:           http.AllowQuerySemicolons(mux),
		ReadHeaderTimeout: 10 * time.Second, // Mitigate Slow Loris Attack
		ErrorLog:          log.New(io.Discard, "", 0),
		// Against good practice no timeouts here, otherwise big files would be terminated when downloaded
	}

	// init clipboard
	if !fs.NoClipboard {
		fs.Clipboard = clipboard.New()

		// init websocket hub
		fs.Hub = ws.NewHub(fs.Clipboard, fs.CLI)
		go fs.Hub.Run()
	}

	// Print silent banner
	if fs.Silent {
		logger.Info("Serving in silent mode - no dir listing available at HTTP Listener")
	}

	// Print all embedded files as info to the console
	fs.PrintEmbeddedFiles()

	// Register webhook
	if fs.WebhookEnable {
		switch strings.ToLower(fs.WebhookProvider) {
		case "discord":
			fs.Webhook = &webhook.DiscordWebhook{
				URL:      fs.WebhookURL,
				Username: "goshs",
			}
		case "slack":
			fs.Webhook = &webhook.SlackWebhook{
				URL: fs.WebhookURL,
			}
		case "mattermost":
			fs.Webhook = &webhook.MattermostWebhook{
				URL:      fs.WebhookURL,
				Username: "goshs",
			}
		default:
			logger.Fatalf("Webhook provider '%s' not supported", fs.WebhookProvider)
		}
	}

	// Start listener
	fs.StartListener(server, what, listener)
}

func (c *Client) dispatchReadPump(packet Packet) {
	// Switch here over possible socket events and pull in handlers
	switch packet.Type {
	case "newEntry":
		var entry string
		if err := json.Unmarshal(packet.Content, &entry); err != nil {
			logger.Errorf("Error reading json packet: %+v", err)
		}
		if err := c.hub.cb.AddEntry(entry); err != nil {
			logger.Errorf("Error creating Clipboard entry: %+v", err)
		}
		c.refreshClipboard()

	case "delEntry":
		var id string
		if err := json.Unmarshal(packet.Content, &id); err != nil {
			logger.Errorf("Error reading json packet: %+v", err)
		}
		iid, err := strconv.Atoi(id)
		if err != nil {
			logger.Errorf("Error reading json packet: %+v", err)
		}
		if err := c.hub.cb.DeleteEntry(iid); err != nil {
			logger.Errorf("Error to delete Clipboard entry with id: %s: %+v", string(packet.Content), err)
		}
		c.refreshClipboard()

	case "clearClipboard":
		if err := c.hub.cb.ClearClipboard(); err != nil {
			logger.Errorf("Error clearing clipboard: %+v", err)
		}
		c.refreshClipboard()

	case "command":
		if c.hub.cliEnabled {
			var command string
			if err := json.Unmarshal(packet.Content, &command); err != nil {
				logger.Errorf("Error reading json packet: %+v", err)
			}
			logger.Debugf("Command was: %+v", command)
			output, err := cli.RunCMD(command)
			if err != nil {
				logger.Errorf("Error running command: %+v", err)
			}
			logger.Debugf("Output: %+v", output)
			c.updateCLI(output)
		}

	default:
		logger.Warnf("The event sent via websocket cannot be handeled: %+v", packet.Type)
	}

}

type Hub struct {
	// Registered clients.
	clients map[*Client]bool

	// Inbound messages from the clients.
	broadcast chan []byte

	// Register requests from the clients.
	register chan *Client

	// Unregister requests from clients.
	unregister chan *Client

	// Handle clipboard
	cb *clipboard.Clipboard

	// CLI Enabled
	cliEnabled bool
}

func NewHub(cb *clipboard.Clipboard, cliEnabled bool) *Hub {
	return &Hub{
		broadcast:  make(chan []byte),
		register:   make(chan *Client),
		unregister: make(chan *Client),
		clients:    make(map[*Client]bool),
		cb:         cb,
		cliEnabled: cliEnabled,
	}
}

--- a/httpserver/server.go
+++ b/httpserver/server.go
@@ -29,7 +29,7 @@
 		fs.Clipboard = clipboard.New()

 		// init websocket hub
-		fs.Hub = ws.NewHub(fs.Clipboard)
+		fs.Hub = ws.NewHub(fs.Clipboard, fs.CLI)
 		go fs.Hub.Run()
 	}

--- a/ws/client.go
+++ b/ws/client.go
@@ -32,17 +32,19 @@
 		c.refreshClipboard()

 	case "command":
-		var command string
-		if err := json.Unmarshal(packet.Content, &command); err != nil {
-			logger.Errorf("Error reading json packet: %+v", err)
+		if c.hub.cliEnabled {
+			var command string
+			if err := json.Unmarshal(packet.Content, &command); err != nil {
+				logger.Errorf("Error reading json packet: %+v", err)
+			}
+			logger.Debugf("Command was: %+v", command)
+			output, err := cli.RunCMD(command)
+			if err != nil {
+				logger.Errorf("Error running command: %+v", err)
+			}
+			logger.Debugf("Output: %+v", output)
+			c.updateCLI(output)
 		}
-		logger.Debugf("Command was: %+v", command)
-		output, err := cli.RunCMD(command)
-		if err != nil {
-			logger.Errorf("Error running command: %+v", err)
-		}
-		logger.Debugf("Output: %+v", output)
-		c.updateCLI(output)

 	default:
 		logger.Warnf("The event sent via websocket cannot be handeled: %+v", packet.Type)

--- a/ws/hub.go
+++ b/ws/hub.go
@@ -13,4 +13,7 @@

 	// Handle clipboard
 	cb *clipboard.Clipboard
+
+	// CLI Enabled
+	cliEnabled bool
 }

--- a/ws/hub.go
+++ b/ws/hub.go
@@ -1,9 +1,10 @@
-func NewHub(cb *clipboard.Clipboard) *Hub {
+func NewHub(cb *clipboard.Clipboard, cliEnabled bool) *Hub {
 	return &Hub{
 		broadcast:  make(chan []byte),
 		register:   make(chan *Client),
 		unregister: make(chan *Client),
 		clients:    make(map[*Client]bool),
 		cb:         cb,
+		cliEnabled: cliEnabled,
 	}
 }


Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. The vulnerability allows unauthorized access to the sensitive settings exposed by  /api/v1/settings endpoint without authentication. All sensitive settings are hidden except passwordPattern. This vulnerability is fixed in 2.11.3, 2.10.12, and 2.9.17.

Missing Authentication for Critical Function

The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.

func (s *Server) Get(ctx context.Context, q *settingspkg.SettingsQuery) (*settingspkg.Settings, error) {
	resourceOverrides, err := s.mgr.GetResourceOverrides()
	if err != nil {
		return nil, err
	}
	overrides := make(map[string]*v1alpha1.ResourceOverride)
	for k := range resourceOverrides {
		val := resourceOverrides[k]
		overrides[k] = &val
	}
	appInstanceLabelKey, err := s.mgr.GetAppInstanceLabelKey()
	if err != nil {
		return nil, err
	}
	argoCDSettings, err := s.mgr.GetSettings()
	if err != nil {
		return nil, err
	}
	gaSettings, err := s.mgr.GetGoogleAnalytics()
	if err != nil {
		return nil, err
	}
	help, err := s.mgr.GetHelp()
	if err != nil {
		return nil, err
	}
	userLoginsDisabled := true
	accounts, err := s.mgr.GetAccounts()
	if err != nil {
		return nil, err
	}
	for _, account := range accounts {
		if account.Enabled && account.HasCapability(settings.AccountCapabilityLogin) {
			userLoginsDisabled = false
			break
		}
	}

	kustomizeSettings, err := s.mgr.GetKustomizeSettings()
	if err != nil {
		return nil, err
	}
	var kustomizeVersions []string
	for i := range kustomizeSettings.Versions {
		kustomizeVersions = append(kustomizeVersions, kustomizeSettings.Versions[i].Name)
	}

	trackingMethod, err := s.mgr.GetTrackingMethod()
	if err != nil {
		return nil, err
	}

	set := settingspkg.Settings{
		URL:                argoCDSettings.URL,
		AppLabelKey:        appInstanceLabelKey,
		ResourceOverrides:  overrides,
		StatusBadgeEnabled: argoCDSettings.StatusBadgeEnabled,
		StatusBadgeRootUrl: argoCDSettings.StatusBadgeRootUrl,
		KustomizeOptions: &v1alpha1.KustomizeOptions{
			BuildOptions: argoCDSettings.KustomizeBuildOptions,
		},
		GoogleAnalytics: &settingspkg.GoogleAnalyticsConfig{
			TrackingID:     gaSettings.TrackingID,
			AnonymizeUsers: gaSettings.AnonymizeUsers,
		},
		Help: &settingspkg.Help{
			ChatUrl:    help.ChatURL,
			ChatText:   help.ChatText,
			BinaryUrls: help.BinaryURLs,
		},
		UserLoginsDisabled:        userLoginsDisabled,
		KustomizeVersions:         kustomizeVersions,
		UiCssURL:                  argoCDSettings.UiCssURL,
		PasswordPattern:           argoCDSettings.PasswordPattern,
		TrackingMethod:            trackingMethod,
		ExecEnabled:               argoCDSettings.ExecEnabled,
		AppsInAnyNamespaceEnabled: s.appsInAnyNamespaceEnabled,
	}

	if sessionmgr.LoggedIn(ctx) || s.disableAuth {
		set.UiBannerContent = argoCDSettings.UiBannerContent
		set.UiBannerURL = argoCDSettings.UiBannerURL
		set.UiBannerPermanent = argoCDSettings.UiBannerPermanent
		set.UiBannerPosition = argoCDSettings.UiBannerPosition
		set.ControllerNamespace = s.mgr.GetNamespace()
	}
	if argoCDSettings.DexConfig != "" {
		var cfg settingspkg.DexConfig
		err = yaml.Unmarshal([]byte(argoCDSettings.DexConfig), &cfg)
		if err == nil {
			set.DexConfig = &cfg
		}
	}
	if oidcConfig := argoCDSettings.OIDCConfig(); oidcConfig != nil {
		set.OIDCConfig = &settingspkg.OIDCConfig{
			Name:                     oidcConfig.Name,
			Issuer:                   oidcConfig.Issuer,
			ClientID:                 oidcConfig.ClientID,
			CLIClientID:              oidcConfig.CLIClientID,
			Scopes:                   oidcConfig.RequestedScopes,
			EnablePKCEAuthentication: oidcConfig.EnablePKCEAuthentication,
		}
		if len(argoCDSettings.OIDCConfig().RequestedIDTokenClaims) > 0 {
			set.OIDCConfig.IDTokenClaims = argoCDSettings.OIDCConfig().RequestedIDTokenClaims
		}
	}
	return &set, nil
}

func (s *Server) Get(ctx context.Context, q *settingspkg.SettingsQuery) (*settingspkg.Settings, error) {
	resourceOverrides, err := s.mgr.GetResourceOverrides()
	if err != nil {
		return nil, err
	}
	overrides := make(map[string]*v1alpha1.ResourceOverride)
	for k := range resourceOverrides {
		val := resourceOverrides[k]
		overrides[k] = &val
	}
	appInstanceLabelKey, err := s.mgr.GetAppInstanceLabelKey()
	if err != nil {
		return nil, err
	}
	argoCDSettings, err := s.mgr.GetSettings()
	if err != nil {
		return nil, err
	}
	gaSettings, err := s.mgr.GetGoogleAnalytics()
	if err != nil {
		return nil, err
	}
	help, err := s.mgr.GetHelp()
	if err != nil {
		return nil, err
	}
	userLoginsDisabled := true
	accounts, err := s.mgr.GetAccounts()
	if err != nil {
		return nil, err
	}
	for _, account := range accounts {
		if account.Enabled && account.HasCapability(settings.AccountCapabilityLogin) {
			userLoginsDisabled = false
			break
		}
	}

	kustomizeSettings, err := s.mgr.GetKustomizeSettings()
	if err != nil {
		return nil, err
	}
	var kustomizeVersions []string
	for i := range kustomizeSettings.Versions {
		kustomizeVersions = append(kustomizeVersions, kustomizeSettings.Versions[i].Name)
	}

	trackingMethod, err := s.mgr.GetTrackingMethod()
	if err != nil {
		return nil, err
	}

	set := settingspkg.Settings{
		URL:                argoCDSettings.URL,
		AppLabelKey:        appInstanceLabelKey,
		ResourceOverrides:  overrides,
		StatusBadgeEnabled: argoCDSettings.StatusBadgeEnabled,
		StatusBadgeRootUrl: argoCDSettings.StatusBadgeRootUrl,
		KustomizeOptions: &v1alpha1.KustomizeOptions{
			BuildOptions: argoCDSettings.KustomizeBuildOptions,
		},
		GoogleAnalytics: &settingspkg.GoogleAnalyticsConfig{
			TrackingID:     gaSettings.TrackingID,
			AnonymizeUsers: gaSettings.AnonymizeUsers,
		},
		Help: &settingspkg.Help{
			ChatUrl:    help.ChatURL,
			ChatText:   help.ChatText,
			BinaryUrls: help.BinaryURLs,
		},
		UserLoginsDisabled:        userLoginsDisabled,
		KustomizeVersions:         kustomizeVersions,
		UiCssURL:                  argoCDSettings.UiCssURL,
		TrackingMethod:            trackingMethod,
		ExecEnabled:               argoCDSettings.ExecEnabled,
		AppsInAnyNamespaceEnabled: s.appsInAnyNamespaceEnabled,
	}

	if sessionmgr.LoggedIn(ctx) || s.disableAuth {
		set.UiBannerContent = argoCDSettings.UiBannerContent
		set.UiBannerURL = argoCDSettings.UiBannerURL
		set.UiBannerPermanent = argoCDSettings.UiBannerPermanent
		set.UiBannerPosition = argoCDSettings.UiBannerPosition
		set.ControllerNamespace = s.mgr.GetNamespace()
	}
	if sessionmgr.LoggedIn(ctx) {
		set.PasswordPattern = argoCDSettings.PasswordPattern
	}
	if argoCDSettings.DexConfig != "" {
		var cfg settingspkg.DexConfig
		err = yaml.Unmarshal([]byte(argoCDSettings.DexConfig), &cfg)
		if err == nil {
			set.DexConfig = &cfg
		}
	}
	if oidcConfig := argoCDSettings.OIDCConfig(); oidcConfig != nil {
		set.OIDCConfig = &settingspkg.OIDCConfig{
			Name:                     oidcConfig.Name,
			Issuer:                   oidcConfig.Issuer,
			ClientID:                 oidcConfig.ClientID,
			CLIClientID:              oidcConfig.CLIClientID,
			Scopes:                   oidcConfig.RequestedScopes,
			EnablePKCEAuthentication: oidcConfig.EnablePKCEAuthentication,
		}
		if len(argoCDSettings.OIDCConfig().RequestedIDTokenClaims) > 0 {
			set.OIDCConfig.IDTokenClaims = argoCDSettings.OIDCConfig().RequestedIDTokenClaims
		}
	}
	return &set, nil
}

--- a/server/settings/settings.go
+++ b/server/settings/settings.go
@@ -71,7 +71,6 @@
 		UserLoginsDisabled:        userLoginsDisabled,
 		KustomizeVersions:         kustomizeVersions,
 		UiCssURL:                  argoCDSettings.UiCssURL,
-		PasswordPattern:           argoCDSettings.PasswordPattern,
 		TrackingMethod:            trackingMethod,
 		ExecEnabled:               argoCDSettings.ExecEnabled,
 		AppsInAnyNamespaceEnabled: s.appsInAnyNamespaceEnabled,
@@ -83,6 +82,9 @@
 		set.UiBannerPermanent = argoCDSettings.UiBannerPermanent
 		set.UiBannerPosition = argoCDSettings.UiBannerPosition
 		set.ControllerNamespace = s.mgr.GetNamespace()
+	}
+	if sessionmgr.LoggedIn(ctx) {
+		set.PasswordPattern = argoCDSettings.PasswordPattern
 	}
 	if argoCDSettings.DexConfig != "" {
 		var cfg settingspkg.DexConfig


OpenFGA is an authorization/permission engine. Versions prior to version 0.2.4 are vulnerable to authorization bypass under certain conditions. Users whose model has a relation defined as a tupleset (the right hand side of a ‘from’ statement) that involves anything other than a direct relationship (e.g. ‘as self’) are vulnerable. Version 0.2.4 contains a patch for this issue.

Improper Authorization

The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.

Execution with Unnecessary Privileges

The product performs an operation at a privilege level that is higher than the minimum level required, which creates new weaknesses or amplifies the consequences of other weaknesses.

Improper Privilege Management

The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.

func validateRelationRewrites(model *openfgapb.AuthorizationModel) error {
	typeDefinitions := model.GetTypeDefinitions()

	allRelations := map[string]struct{}{}
	typeToRelations := map[string]map[string]struct{}{}
	for _, td := range typeDefinitions {
		objectType := td.GetType()
		typeToRelations[objectType] = map[string]struct{}{}
		for relation := range td.GetRelations() {
			typeToRelations[objectType][relation] = struct{}{}
			allRelations[relation] = struct{}{}
		}
	}

	for _, td := range typeDefinitions {
		objectType := td.GetType()
		for relation, rewrite := range td.GetRelations() {
			err := isUsersetRewriteValid(allRelations, typeToRelations[objectType], objectType, relation, rewrite)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func isUsersetRewriteValid(allRelations map[string]struct{}, relationsOnType map[string]struct{}, objectType, relation string, rewrite *openfgapb.Userset) error {
	if rewrite.GetUserset() == nil {
		return InvalidRelationError(objectType, relation)
	}

	switch t := rewrite.GetUserset().(type) {
	case *openfgapb.Userset_ComputedUserset:
		computedUserset := t.ComputedUserset.GetRelation()
		if computedUserset == relation {
			return InvalidRelationError(objectType, relation)
		}
		if _, ok := relationsOnType[computedUserset]; !ok {
			return RelationDoesNotExistError(objectType, computedUserset)
		}
	case *openfgapb.Userset_TupleToUserset:
		tupleset := t.TupleToUserset.GetTupleset().GetRelation()
		if _, ok := relationsOnType[tupleset]; !ok {
			return RelationDoesNotExistError(objectType, tupleset)
		}

		computedUserset := t.TupleToUserset.GetComputedUserset().GetRelation()
		if _, ok := allRelations[computedUserset]; !ok {
			return RelationDoesNotExistError("", computedUserset)
		}
	case *openfgapb.Userset_Union:
		for _, child := range t.Union.GetChild() {
			err := isUsersetRewriteValid(allRelations, relationsOnType, objectType, relation, child)
			if err != nil {
				return err
			}
		}
	case *openfgapb.Userset_Intersection:
		for _, child := range t.Intersection.GetChild() {
			err := isUsersetRewriteValid(allRelations, relationsOnType, objectType, relation, child)
			if err != nil {
				return err
			}
		}
	case *openfgapb.Userset_Difference:
		err := isUsersetRewriteValid(allRelations, relationsOnType, objectType, relation, t.Difference.Base)
		if err != nil {
			return err
		}

		err = isUsersetRewriteValid(allRelations, relationsOnType, objectType, relation, t.Difference.Subtract)
		if err != nil {
			return err
		}
	}

	return nil
}

func validateRelationRewrites(model *openfgapb.AuthorizationModel) error {
	typeDefinitions := model.GetTypeDefinitions()

	relations := map[string]*openfgapb.Relation{}
	typerels := map[string]map[string]*openfgapb.Relation{}

	for _, td := range typeDefinitions {
		objectType := td.GetType()

		typerels[objectType] = map[string]*openfgapb.Relation{}

		for relation, rewrite := range td.GetRelations() {
			relationMetadata := td.GetMetadata().GetRelations()
			md, ok := relationMetadata[relation]

			var typeinfo *openfgapb.RelationTypeInfo
			if ok {
				typeinfo = &openfgapb.RelationTypeInfo{
					DirectlyRelatedUserTypes: md.GetDirectlyRelatedUserTypes(),
				}
			}

			r := &openfgapb.Relation{
				Name:     relation,
				Rewrite:  rewrite,
				TypeInfo: typeinfo,
			}

			typerels[objectType][relation] = r
			relations[relation] = r
		}
	}

	for _, td := range typeDefinitions {
		objectType := td.GetType()

		for relation, rewrite := range td.GetRelations() {
			err := isUsersetRewriteValid(relations, typerels[objectType], objectType, relation, rewrite)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func isUsersetRewriteValid(
	allRelations map[string]*openfgapb.Relation,
	relationsOnType map[string]*openfgapb.Relation,
	objectType, relation string,
	rewrite *openfgapb.Userset,
) error {
	if rewrite.GetUserset() == nil {
		return InvalidRelationError(objectType, relation)
	}

	switch t := rewrite.GetUserset().(type) {
	case *openfgapb.Userset_ComputedUserset:
		computedUserset := t.ComputedUserset.GetRelation()
		if computedUserset == relation {
			return InvalidRelationError(objectType, relation)
		}
		if _, ok := relationsOnType[computedUserset]; !ok {
			return RelationDoesNotExistError(objectType, computedUserset)
		}
	case *openfgapb.Userset_TupleToUserset:
		tupleset := t.TupleToUserset.GetTupleset().GetRelation()

		tuplesetRelation, ok := relationsOnType[tupleset]
		if !ok {
			return RelationDoesNotExistError(objectType, tupleset)
		}

		// tupleset relations must only be direct relationships, no rewrites
		// are allowed on them
		tuplesetRewrite := tuplesetRelation.GetRewrite()
		if reflect.TypeOf(tuplesetRewrite.GetUserset()) != reflect.TypeOf(&openfgapb.Userset_This{}) {
			return errors.Errorf("the '%s#%s' relation is referenced in at least one tupleset and thus must be a direct relation", objectType, tupleset)
		}

		computedUserset := t.TupleToUserset.GetComputedUserset().GetRelation()
		if _, ok := allRelations[computedUserset]; !ok {
			return RelationDoesNotExistError("", computedUserset)
		}
	case *openfgapb.Userset_Union:
		for _, child := range t.Union.GetChild() {
			err := isUsersetRewriteValid(allRelations, relationsOnType, objectType, relation, child)
			if err != nil {
				return err
			}
		}
	case *openfgapb.Userset_Intersection:
		for _, child := range t.Intersection.GetChild() {
			err := isUsersetRewriteValid(allRelations, relationsOnType, objectType, relation, child)
			if err != nil {
				return err
			}
		}
	case *openfgapb.Userset_Difference:
		err := isUsersetRewriteValid(allRelations, relationsOnType, objectType, relation, t.Difference.Base)
		if err != nil {
			return err
		}

		err = isUsersetRewriteValid(allRelations, relationsOnType, objectType, relation, t.Difference.Subtract)
		if err != nil {
			return err
		}
	}

	return nil
}

--- a/pkg/typesystem/typesystem.go
+++ b/pkg/typesystem/typesystem.go
@@ -1,21 +1,41 @@
 func validateRelationRewrites(model *openfgapb.AuthorizationModel) error {
 	typeDefinitions := model.GetTypeDefinitions()

-	allRelations := map[string]struct{}{}
-	typeToRelations := map[string]map[string]struct{}{}
+	relations := map[string]*openfgapb.Relation{}
+	typerels := map[string]map[string]*openfgapb.Relation{}
+
 	for _, td := range typeDefinitions {
 		objectType := td.GetType()
-		typeToRelations[objectType] = map[string]struct{}{}
-		for relation := range td.GetRelations() {
-			typeToRelations[objectType][relation] = struct{}{}
-			allRelations[relation] = struct{}{}
+
+		typerels[objectType] = map[string]*openfgapb.Relation{}
+
+		for relation, rewrite := range td.GetRelations() {
+			relationMetadata := td.GetMetadata().GetRelations()
+			md, ok := relationMetadata[relation]
+
+			var typeinfo *openfgapb.RelationTypeInfo
+			if ok {
+				typeinfo = &openfgapb.RelationTypeInfo{
+					DirectlyRelatedUserTypes: md.GetDirectlyRelatedUserTypes(),
+				}
+			}
+
+			r := &openfgapb.Relation{
+				Name:     relation,
+				Rewrite:  rewrite,
+				TypeInfo: typeinfo,
+			}
+
+			typerels[objectType][relation] = r
+			relations[relation] = r
 		}
 	}

 	for _, td := range typeDefinitions {
 		objectType := td.GetType()
+
 		for relation, rewrite := range td.GetRelations() {
-			err := isUsersetRewriteValid(allRelations, typeToRelations[objectType], objectType, relation, rewrite)
+			err := isUsersetRewriteValid(relations, typerels[objectType], objectType, relation, rewrite)
 			if err != nil {
 				return err
 			}

--- a/pkg/typesystem/typesystem.go
+++ b/pkg/typesystem/typesystem.go
@@ -1,4 +1,9 @@
-func isUsersetRewriteValid(allRelations map[string]struct{}, relationsOnType map[string]struct{}, objectType, relation string, rewrite *openfgapb.Userset) error {
+func isUsersetRewriteValid(
+	allRelations map[string]*openfgapb.Relation,
+	relationsOnType map[string]*openfgapb.Relation,
+	objectType, relation string,
+	rewrite *openfgapb.Userset,
+) error {
 	if rewrite.GetUserset() == nil {
 		return InvalidRelationError(objectType, relation)
 	}
@@ -14,8 +19,17 @@
 		}
 	case *openfgapb.Userset_TupleToUserset:
 		tupleset := t.TupleToUserset.GetTupleset().GetRelation()
-		if _, ok := relationsOnType[tupleset]; !ok {
+
+		tuplesetRelation, ok := relationsOnType[tupleset]
+		if !ok {
 			return RelationDoesNotExistError(objectType, tupleset)
+		}
+
+		// tupleset relations must only be direct relationships, no rewrites
+		// are allowed on them
+		tuplesetRewrite := tuplesetRelation.GetRewrite()
+		if reflect.TypeOf(tuplesetRewrite.GetUserset()) != reflect.TypeOf(&openfgapb.Userset_This{}) {
+			return errors.Errorf("the '%s#%s' relation is referenced in at least one tupleset and thus must be a direct relation", objectType, tupleset)
 		}

 		computedUserset := t.TupleToUserset.GetComputedUserset().GetRelation()


This affects the package connection-tester before 0.2.1. The injection point is located in line 15 in index.js. The following PoC demonstrates the vulnerability:

Improper Neutralization of Special Elements used in a Command ('Command Injection')

The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.

    test: function ConnectionTester(host, port, callbackOrConnectTimeout, callback) {

        // for backward compatibility
        if (typeof callbackOrConnectTimeout === 'function') {
            console.log('deprecated: Please migrate to the new interface ConnectionTester\(host, port, timeout, callback\)');
            return testAsync(host, port, SOCKET_TIMEOUT, callbackOrConnectTimeout);
        }
        if (typeof callbackOrConnectTimeout === 'number') {
            if (callback) {
                return testAsync(host, port, callbackOrConnectTimeout, callback);
            } else {
                return testSync(host, port, callbackOrConnectTimeout);
            }
        }
        if (callbackOrConnectTimeout === undefined) {
            return testSync(host, port, SOCKET_TIMEOUT);
        }
    }

    test: function ConnectionTester(host, port, connectTimeout, callback) {

        // validate host
        if (!isValidHostNameOrIP(host)) {
            console.error('[connection-tester] invalid host: ', host);
            host = undefined;
        }
        // validate port
        var originalPort = port;
        port = +port;
        if (!port || port < 0 || port > 65535) {
            console.error('[connection-tester] invalid port: ', originalPort);
            port = undefined;
        }

        if (typeof connectTimeout === 'function') {
            console.error('deprecated: Please migrate to the new interface ConnectionTester\(host, port, timeout, callback\)');
            callback = connectTimeout;
            connectTimeout = SOCKET_TIMEOUT;
        }
        if (connectTimeout === undefined) {
            connectTimeout = SOCKET_TIMEOUT;
        }

        if (typeof connectTimeout === 'number') {
            if (!port || !host) {
                var output = {
                    success: false,
                    error: 'invalid host/port'
                };

                if (callback) {
                    return callback(null, output);
                } else {
                    return output;
                }
            }
            if (callback) {
                return testAsync(host, port, connectTimeout, callback);
            } else {
                return testSync(host, port, connectTimeout);
            }
        }
    }

//source - http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
var ValidHostnameRegex = new RegExp("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$");

function isValidHostNameOrIP(host) {
    return net.isIP(host) || ValidHostnameRegex.test(host);
}

--- a/index.js
+++ b/index.js
@@ -1,18 +1,44 @@
-    test: function ConnectionTester(host, port, callbackOrConnectTimeout, callback) {
+    test: function ConnectionTester(host, port, connectTimeout, callback) {

-        // for backward compatibility
-        if (typeof callbackOrConnectTimeout === 'function') {
-            console.log('deprecated: Please migrate to the new interface ConnectionTester\(host, port, timeout, callback\)');
-            return testAsync(host, port, SOCKET_TIMEOUT, callbackOrConnectTimeout);
+        // validate host
+        if (!isValidHostNameOrIP(host)) {
+            console.error('[connection-tester] invalid host: ', host);
+            host = undefined;
         }
-        if (typeof callbackOrConnectTimeout === 'number') {
+        // validate port
+        var originalPort = port;
+        port = +port;
+        if (!port || port < 0 || port > 65535) {
+            console.error('[connection-tester] invalid port: ', originalPort);
+            port = undefined;
+        }
+
+        if (typeof connectTimeout === 'function') {
+            console.error('deprecated: Please migrate to the new interface ConnectionTester\(host, port, timeout, callback\)');
+            callback = connectTimeout;
+            connectTimeout = SOCKET_TIMEOUT;
+        }
+        if (connectTimeout === undefined) {
+            connectTimeout = SOCKET_TIMEOUT;
+        }
+
+        if (typeof connectTimeout === 'number') {
+            if (!port || !host) {
+                var output = {
+                    success: false,
+                    error: 'invalid host/port'
+                };
+
+                if (callback) {
+                    return callback(null, output);
+                } else {
+                    return output;
+                }
+            }
             if (callback) {
-                return testAsync(host, port, callbackOrConnectTimeout, callback);
+                return testAsync(host, port, connectTimeout, callback);
             } else {
-                return testSync(host, port, callbackOrConnectTimeout);
+                return testSync(host, port, connectTimeout);
             }
         }
-        if (callbackOrConnectTimeout === undefined) {
-            return testSync(host, port, SOCKET_TIMEOUT);
-        }
     }

--- /dev/null
+++ b/index.js
@@ -0,0 +1,6 @@
+//source - http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
+var ValidHostnameRegex = new RegExp("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$");
+
+function isValidHostNameOrIP(host) {
+    return net.isIP(host) || ValidHostnameRegex.test(host);
+}

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2020-7781:latest
# bash /workspace/fix-run.sh
set -e

cd /workspace/node-connection-tester
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
jest ./poc

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2020-7781:latest
# bash /workspace/unit_test.sh
set -e

cd /workspace/node-connection-tester
git apply --whitespace=nowarn /workspace/fix.patch
npx mocha --grep "should connect to localhost|should return false while connecting to 5678 port on www.example.com|should connect to localhost|should connect to www.yahoo.com 80|should return false while connecting to dead port on localhost"


Server-Side Request Forgery (SSRF) in GitHub repository janeczku/calibre-web prior to 0.6.17.

def save_cover_from_url(url, book_path):
    try:
        if not cli.allow_localhost:
            # 127.0.x.x, localhost, [::1], [::ffff:7f00:1]
            ip = socket.getaddrinfo(urlparse(url).hostname, 0)[0][4][0]
            if ip.startswith("127.") or ip.startswith('::ffff:7f') or ip == "::1":
                log.error("Localhost was accessed for cover upload")
                return False, _("You are not allowed to access localhost for cover uploads")
        img = requests.get(url, timeout=(10, 200))      # ToDo: Error Handling
        img.raise_for_status()
        return save_cover(img, book_path)
    except (socket.gaierror,
            requests.exceptions.HTTPError,
            requests.exceptions.ConnectionError,
            requests.exceptions.Timeout) as ex:
        log.info(u'Cover Download Error %s', ex)
        return False, _("Error Downloading Cover")
    except MissingDelegateError as ex:
        log.info(u'File Format Error %s', ex)
        return False, _("Cover Format Error")

def save_cover_from_url(url, book_path):
    try:
        if not cli.allow_localhost:
            # 127.0.x.x, localhost, [::1], [::ffff:7f00:1]
            ip = socket.getaddrinfo(urlparse(url).hostname, 0)[0][4][0]
            if ip.startswith("127.") or ip.startswith('::ffff:7f') or ip == "::1" or ip == "0.0.0.0" or ip == "::":
                log.error("Localhost was accessed for cover upload")
                return False, _("You are not allowed to access localhost for cover uploads")
        img = requests.get(url, timeout=(10, 200), allow_redirects=False)      # ToDo: Error Handling
        img.raise_for_status()
        return save_cover(img, book_path)
    except (socket.gaierror,
            requests.exceptions.HTTPError,
            requests.exceptions.ConnectionError,
            requests.exceptions.Timeout) as ex:
        log.info(u'Cover Download Error %s', ex)
        return False, _("Error Downloading Cover")
    except MissingDelegateError as ex:
        log.info(u'File Format Error %s', ex)
        return False, _("Cover Format Error")

--- a/cps/helper.py
+++ b/cps/helper.py
@@ -3,10 +3,10 @@
         if not cli.allow_localhost:
             # 127.0.x.x, localhost, [::1], [::ffff:7f00:1]
             ip = socket.getaddrinfo(urlparse(url).hostname, 0)[0][4][0]
-            if ip.startswith("127.") or ip.startswith('::ffff:7f') or ip == "::1":
+            if ip.startswith("127.") or ip.startswith('::ffff:7f') or ip == "::1" or ip == "0.0.0.0" or ip == "::":
                 log.error("Localhost was accessed for cover upload")
                 return False, _("You are not allowed to access localhost for cover uploads")
-        img = requests.get(url, timeout=(10, 200))      # ToDo: Error Handling
+        img = requests.get(url, timeout=(10, 200), allow_redirects=False)      # ToDo: Error Handling
         img.raise_for_status()
         return save_cover(img, book_path)
     except (socket.gaierror,


With this vulnerability an attacker can read many sensitive files like configuration files, or the /proc/self/environ file, that contains the environment variable used by the web server that includes database credentials. If the web server user is root, an attacker will be able to read any file in the system.

module.exports = {
  inputs: {
    id: {
      type: 'string',
      regex: /^[0-9]+$/,
      required: true,
    },
    filename: {
      type: 'string',
      required: true,
    },
  },

  async fn(inputs, exits) {
    const { currentUser } = this.req;

    const { attachment, card, project } = await sails.helpers.attachments
      .getProjectPath(inputs.id)
      .intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND);

    const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);

    if (!isBoardMember) {
      const isProjectManager = await sails.helpers.users.isProjectManager(
        currentUser.id,
        project.id,
      );

      if (!isProjectManager) {
        throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
      }
    }

    if (!attachment.image) {
      throw Errors.ATTACHMENT_NOT_FOUND;
    }

    const filePath = path.join(
      sails.config.custom.attachmentsPath,
      attachment.dirname,
      'thumbnails',
      inputs.filename,
    );

    if (!fs.existsSync(filePath)) {
      throw Errors.ATTACHMENT_NOT_FOUND;
    }

    this.res.type(attachment.filename);
    this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config

    return exits.success(fs.createReadStream(filePath));
  },
};

  'GET /attachments/:id/download/thumbnails/:filename': {
    action: 'attachments/download-thumbnail',
    skipAssets: false,
  },

module.exports = {
  inputs: {
    id: {
      type: 'string',
      regex: /^[0-9]+$/,
      required: true,
    },
  },

  async fn(inputs, exits) {
    const { currentUser } = this.req;

    const { attachment, card, project } = await sails.helpers.attachments
      .getProjectPath(inputs.id)
      .intercept('pathNotFound', () => Errors.ATTACHMENT_NOT_FOUND);

    const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);

    if (!isBoardMember) {
      const isProjectManager = await sails.helpers.users.isProjectManager(
        currentUser.id,
        project.id,
      );

      if (!isProjectManager) {
        throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
      }
    }

    if (!attachment.image) {
      throw Errors.ATTACHMENT_NOT_FOUND;
    }

    const filePath = path.join(
      sails.config.custom.attachmentsPath,
      attachment.dirname,
      'thumbnails',
      'cover-256.jpg',
    );

    if (!fs.existsSync(filePath)) {
      throw Errors.ATTACHMENT_NOT_FOUND;
    }

    this.res.type('image/jpeg');
    this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config

    return exits.success(fs.createReadStream(filePath));
  },
};

--- a/server/api/controllers/attachments/download-thumbnail.js
+++ b/server/api/controllers/attachments/download-thumbnail.js
@@ -5,8 +5,4 @@
       regex: /^[0-9]+$/,
       required: true,
     },
-    filename: {
-      type: 'string',
-      required: true,
-    },
   },

--- a/server/api/controllers/attachments/download-thumbnail.js
+++ b/server/api/controllers/attachments/download-thumbnail.js
@@ -26,14 +26,14 @@
       sails.config.custom.attachmentsPath,
       attachment.dirname,
       'thumbnails',
-      inputs.filename,
+      'cover-256.jpg',
     );

     if (!fs.existsSync(filePath)) {
       throw Errors.ATTACHMENT_NOT_FOUND;
     }

-    this.res.type(attachment.filename);
+    this.res.type('image/jpeg');
     this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config

     return exits.success(fs.createReadStream(filePath));

--- a/server/config/routes.js
+++ b/server/config/routes.js
@@ -1,4 +1,4 @@
-  'GET /attachments/:id/download/thumbnails/:filename': {
+  'GET /attachments/:id/download/thumbnails/cover-256.jpg': {
     action: 'attachments/download-thumbnail',
     skipAssets: false,
   },


Versions of the package langchain-experimental from 0.0.15 and before 0.0.21 are vulnerable to Arbitrary Code Execution when retrieving values from the database, the code will attempt to call 'eval' on all values. An attacker can exploit this vulnerability and execute arbitrary python code if they can control the input prompt and the server is configured with VectorSQLDatabaseChain.

**Notes:**

Impact on the Confidentiality, Integrity and Availability of the vulnerable component:

Confidentiality: Code execution happens within the impacted component, in this case langchain-experimental, so all resources are necessarily accessible.

Integrity: There is nothing protected by the impacted component inherently. Although anything returned from the component counts as 'information' for which the trustworthiness can be compromised.

Availability: The loss of availability isn't caused by the attack itself, but it happens as a result during the attacker's post-exploitation steps.

Impact on the Confidentiality, Integrity and Availability of the subsequent system:

As a legitimate low-privileged user of the package (PR:L) the attacker does not have more access to data owned by the package as a result of this vulnerability than they did with normal usage (e.g. can query the DB). The unintended action that one can perform by breaking out of the app environment and exfiltrating files, making remote connections etc. happens during the post exploitation phase in the subsequent system - in this case, the OS.

AT:P: An attacker needs to be able to influence the input prompt, whilst the server is configured with the VectorSQLDatabaseChain plugin.

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.

def get_result_from_sqldb(
    db: SQLDatabase, cmd: str
) -> Union[str, List[Dict[str, Any]], Dict[str, Any]]:
    result = db._execute(cmd, fetch="all")  # type: ignore
    if isinstance(result, list):
        return [{k: _try_eval(v) for k, v in dict(d._asdict()).items()} for d in result]
    else:
        return {
            k: _try_eval(v) for k, v in dict(result._asdict()).items()  # type: ignore
        }

def get_result_from_sqldb(
    db: SQLDatabase, cmd: str
) -> Union[str, List[Dict[str, Any]], Dict[str, Any]]:
    result = db._execute(cmd, fetch="all")  # type: ignore
    return result

--- a/libs/experimental/langchain_experimental/sql/vector_sql.py
+++ b/libs/experimental/langchain_experimental/sql/vector_sql.py
@@ -2,9 +2,4 @@
     db: SQLDatabase, cmd: str
 ) -> Union[str, List[Dict[str, Any]], Dict[str, Any]]:
     result = db._execute(cmd, fetch="all")  # type: ignore
-    if isinstance(result, list):
-        return [{k: _try_eval(v) for k, v in dict(d._asdict()).items()} for d in result]
-    else:
-        return {
-            k: _try_eval(v) for k, v in dict(result._asdict()).items()  # type: ignore
-        }
+    return result


authentik is an open-source identity provider. Prior to versions 2024.12.4 and 2025.2.3, when authentik was configured to use the database for session storage (which is a non-default setting), deleting sessions via the Web Interface or the API would not revoke the session and the session holder would continue to have access to authentik. authentik 2025.2.3 and 2024.12.4 fix this issue. Switching to the cache-based session storage until the authentik instance can be upgraded is recommended. This will however also delete all existing sessions and users will have to re-authenticate.

Session Fixation

Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.

    def partial_update(self, request: Request, *args, **kwargs) -> Response:
        response = super().partial_update(request, *args, **kwargs)
        instance: User = self.get_object()
        if not instance.is_active:
            sessions = AuthenticatedSession.objects.filter(user=instance)
            session_ids = sessions.values_list("session_key", flat=True)
            cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
            sessions.delete()
            LOGGER.debug("Deleted user's sessions", user=instance.username)
        return response

def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
    """Delete session when authenticated session is deleted"""
    cache_key = f"{KEY_PREFIX}{instance.session_key}"
    cache.delete(cache_key)

    def partial_update(self, request: Request, *args, **kwargs) -> Response:
        response = super().partial_update(request, *args, **kwargs)
        instance: User = self.get_object()
        if not instance.is_active:
            sessions = AuthenticatedSession.objects.filter(user=instance)
            session_ids = sessions.values_list("session_key", flat=True)
            for session in session_ids:
                SessionStore(session).delete()
            sessions.delete()
            LOGGER.debug("Deleted user's sessions", user=instance.username)
        return response

def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
    """Delete session when authenticated session is deleted"""
    SessionStore(instance.session_key).delete()

--- a/authentik/core/api/users.py
+++ b/authentik/core/api/users.py
@@ -4,7 +4,8 @@
         if not instance.is_active:
             sessions = AuthenticatedSession.objects.filter(user=instance)
             session_ids = sessions.values_list("session_key", flat=True)
-            cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
+            for session in session_ids:
+                SessionStore(session).delete()
             sessions.delete()
             LOGGER.debug("Deleted user's sessions", user=instance.username)
         return response

--- a/authentik/core/signals.py
+++ b/authentik/core/signals.py
@@ -1,4 +1,3 @@
 def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
     """Delete session when authenticated session is deleted"""
-    cache_key = f"{KEY_PREFIX}{instance.session_key}"
-    cache.delete(cache_key)
+    SessionStore(instance.session_key).delete()


django.middleware.common.CommonMiddleware in Django 1.11.x before 1.11.15 and 2.0.x before 2.0.8 has an Open Redirect.

URL Redirection to Untrusted Site ('Open Redirect')

The web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a redirect.

    def get_full_path_with_slash(self, request):
        """
        Return the full path of the request with a trailing slash appended.

        Raise a RuntimeError if settings.DEBUG is True and request.method is
        POST, PUT, or PATCH.
        """
        new_path = request.get_full_path(force_append_slash=True)
        if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
            raise RuntimeError(
                "You called this URL via %(method)s, but the URL doesn't end "
                "in a slash and you have APPEND_SLASH set. Django can't "
                "redirect to the slash URL while maintaining %(method)s data. "
                "Change your form to point to %(url)s (note the trailing "
                "slash), or set APPEND_SLASH=False in your Django settings." % {
                    'method': request.method,
                    'url': request.get_host() + new_path,
                }
            )
        return new_path

    def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
        if args and kwargs:
            raise ValueError("Don't mix *args and **kwargs in call to reverse()!")

        if not self._populated:
            self._populate()

        possibilities = self.reverse_dict.getlist(lookup_view)

        for possibility, pattern, defaults, converters in possibilities:
            for result, params in possibility:
                if args:
                    if len(args) != len(params):
                        continue
                    candidate_subs = dict(zip(params, args))
                else:
                    if set(kwargs).symmetric_difference(params).difference(defaults):
                        continue
                    matches = True
                    for k, v in defaults.items():
                        if kwargs.get(k, v) != v:
                            matches = False
                            break
                    if not matches:
                        continue
                    candidate_subs = kwargs
                # Convert the candidate subs to text using Converter.to_url().
                text_candidate_subs = {}
                for k, v in candidate_subs.items():
                    if k in converters:
                        text_candidate_subs[k] = converters[k].to_url(v)
                    else:
                        text_candidate_subs[k] = str(v)
                # WSGI provides decoded URLs, without %xx escapes, and the URL
                # resolver operates on such URLs. First substitute arguments
                # without quoting to build a decoded URL and look for a match.
                # Then, if we have a match, redo the substitution with quoted
                # arguments in order to return a properly encoded URL.
                candidate_pat = _prefix.replace('%', '%%') + result
                if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % text_candidate_subs):
                    # safe characters from `pchar` definition of RFC 3986
                    url = quote(candidate_pat % text_candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
                    # Don't allow construction of scheme relative urls.
                    if url.startswith('//'):
                        url = '/%%2F%s' % url[2:]
                    return url
        # lookup_view can be URL name or callable, but callables are not
        # friendly in error messages.
        m = getattr(lookup_view, '__module__', None)
        n = getattr(lookup_view, '__name__', None)
        if m is not None and n is not None:
            lookup_view_s = "%s.%s" % (m, n)
        else:
            lookup_view_s = lookup_view

        patterns = [pattern for (_, pattern, _, _) in possibilities]
        if patterns:
            if args:
                arg_msg = "arguments '%s'" % (args,)
            elif kwargs:
                arg_msg = "keyword arguments '%s'" % (kwargs,)
            else:
                arg_msg = "no arguments"
            msg = (
                "Reverse for '%s' with %s not found. %d pattern(s) tried: %s" %
                (lookup_view_s, arg_msg, len(patterns), patterns)
            )
        else:
            msg = (
                "Reverse for '%(view)s' not found. '%(view)s' is not "
                "a valid view function or pattern name." % {'view': lookup_view_s}
            )
        raise NoReverseMatch(msg)

    def get_full_path_with_slash(self, request):
        """
        Return the full path of the request with a trailing slash appended.

        Raise a RuntimeError if settings.DEBUG is True and request.method is
        POST, PUT, or PATCH.
        """
        new_path = request.get_full_path(force_append_slash=True)
        # Prevent construction of scheme relative urls.
        new_path = escape_leading_slashes(new_path)
        if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
            raise RuntimeError(
                "You called this URL via %(method)s, but the URL doesn't end "
                "in a slash and you have APPEND_SLASH set. Django can't "
                "redirect to the slash URL while maintaining %(method)s data. "
                "Change your form to point to %(url)s (note the trailing "
                "slash), or set APPEND_SLASH=False in your Django settings." % {
                    'method': request.method,
                    'url': request.get_host() + new_path,
                }
            )
        return new_path

    def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
        if args and kwargs:
            raise ValueError("Don't mix *args and **kwargs in call to reverse()!")

        if not self._populated:
            self._populate()

        possibilities = self.reverse_dict.getlist(lookup_view)

        for possibility, pattern, defaults, converters in possibilities:
            for result, params in possibility:
                if args:
                    if len(args) != len(params):
                        continue
                    candidate_subs = dict(zip(params, args))
                else:
                    if set(kwargs).symmetric_difference(params).difference(defaults):
                        continue
                    matches = True
                    for k, v in defaults.items():
                        if kwargs.get(k, v) != v:
                            matches = False
                            break
                    if not matches:
                        continue
                    candidate_subs = kwargs
                # Convert the candidate subs to text using Converter.to_url().
                text_candidate_subs = {}
                for k, v in candidate_subs.items():
                    if k in converters:
                        text_candidate_subs[k] = converters[k].to_url(v)
                    else:
                        text_candidate_subs[k] = str(v)
                # WSGI provides decoded URLs, without %xx escapes, and the URL
                # resolver operates on such URLs. First substitute arguments
                # without quoting to build a decoded URL and look for a match.
                # Then, if we have a match, redo the substitution with quoted
                # arguments in order to return a properly encoded URL.
                candidate_pat = _prefix.replace('%', '%%') + result
                if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % text_candidate_subs):
                    # safe characters from `pchar` definition of RFC 3986
                    url = quote(candidate_pat % text_candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
                    # Don't allow construction of scheme relative urls.
                    return escape_leading_slashes(url)
        # lookup_view can be URL name or callable, but callables are not
        # friendly in error messages.
        m = getattr(lookup_view, '__module__', None)
        n = getattr(lookup_view, '__name__', None)
        if m is not None and n is not None:
            lookup_view_s = "%s.%s" % (m, n)
        else:
            lookup_view_s = lookup_view

        patterns = [pattern for (_, pattern, _, _) in possibilities]
        if patterns:
            if args:
                arg_msg = "arguments '%s'" % (args,)
            elif kwargs:
                arg_msg = "keyword arguments '%s'" % (kwargs,)
            else:
                arg_msg = "no arguments"
            msg = (
                "Reverse for '%s' with %s not found. %d pattern(s) tried: %s" %
                (lookup_view_s, arg_msg, len(patterns), patterns)
            )
        else:
            msg = (
                "Reverse for '%(view)s' not found. '%(view)s' is not "
                "a valid view function or pattern name." % {'view': lookup_view_s}
            )
        raise NoReverseMatch(msg)

--- a/django/middleware/common.py
+++ b/django/middleware/common.py
@@ -6,6 +6,8 @@
         POST, PUT, or PATCH.
         """
         new_path = request.get_full_path(force_append_slash=True)
+        # Prevent construction of scheme relative urls.
+        new_path = escape_leading_slashes(new_path)
         if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
             raise RuntimeError(
                 "You called this URL via %(method)s, but the URL doesn't end "

--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -41,9 +41,7 @@
                     # safe characters from `pchar` definition of RFC 3986
                     url = quote(candidate_pat % text_candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
                     # Don't allow construction of scheme relative urls.
-                    if url.startswith('//'):
-                        url = '/%%2F%s' % url[2:]
-                    return url
+                    return escape_leading_slashes(url)
         # lookup_view can be URL name or callable, but callables are not
         # friendly in error messages.
         m = getattr(lookup_view, '__module__', None)

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2018-14574:latest
# bash /workspace/fix-run.sh
set -e

cd /workspace/django
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
cd tests && /workspace/PoC_env/CVE-2018-14574/bin/python ./runtests.py middleware.tests.CommonMiddlewareTest.test_append_slash_leading_slashes utils_tests.test_http.EscapeLeadingSlashesTests

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2018-14574:latest
# bash /workspace/unit_test.sh
set -e

cd /workspace/django
git apply --whitespace=nowarn /workspace/fix.patch
cd tests && /workspace/PoC_env/CVE-2018-14574/bin/python ./runtests.py middleware.tests


A path traversal vulnerability exists in mudler/localai version 2.14.0, where an attacker can exploit the `model` parameter during the model deletion process to delete arbitrary files. Specifically, by crafting a request with a manipulated `model` parameter, an attacker can traverse the directory structure and target files outside of the intended directory, leading to the deletion of sensitive data. This vulnerability is due to insufficient input validation and sanitization of the `model` parameter.

External Control of File Name or Path

The product allows user input to control or influence paths or file names that are used in filesystem operations.

func (cm *BackendConfigLoader) LoadBackendConfigFile(file string, opts ...ConfigLoaderOption) error {
	cm.Lock()
	defer cm.Unlock()
	c, err := ReadBackendConfigFile(file, opts...)
	if err != nil {
		return fmt.Errorf("cannot load config file: %w", err)
	}

	for _, cc := range c {
		cm.configs[cc.Name] = *cc
	}
	return nil
}

func (cl *BackendConfigLoader) LoadBackendConfig(file string, opts ...ConfigLoaderOption) error {
	cl.Lock()
	defer cl.Unlock()
	c, err := ReadBackendConfig(file, opts...)
	if err != nil {
		return fmt.Errorf("cannot read config file: %w", err)
	}

	cl.configs[c.Name] = *c
	return nil
}

func (cm *BackendConfigLoader) LoadBackendConfigsFromPath(path string, opts ...ConfigLoaderOption) error {
	cm.Lock()
	defer cm.Unlock()
	entries, err := os.ReadDir(path)
	if err != nil {
		return err
	}
	files := make([]fs.FileInfo, 0, len(entries))
	for _, entry := range entries {
		info, err := entry.Info()
		if err != nil {
			return err
		}
		files = append(files, info)
	}
	for _, file := range files {
		// Skip templates, YAML and .keep files
		if !strings.Contains(file.Name(), ".yaml") && !strings.Contains(file.Name(), ".yml") ||
			strings.HasPrefix(file.Name(), ".") {
			continue
		}
		c, err := ReadBackendConfig(filepath.Join(path, file.Name()), opts...)
		if err == nil {
			cm.configs[c.Name] = *c
		}
	}

	return nil
}

func (cm *BackendConfigLoader) LoadBackendConfigFile(file string, opts ...ConfigLoaderOption) error {
	cm.Lock()
	defer cm.Unlock()
	c, err := ReadBackendConfigFile(file, opts...)
	if err != nil {
		return fmt.Errorf("cannot load config file: %w", err)
	}

	for _, cc := range c {
		if cc.Validate() {
			cm.configs[cc.Name] = *cc
		}
	}
	return nil
}

func (cl *BackendConfigLoader) LoadBackendConfig(file string, opts ...ConfigLoaderOption) error {
	cl.Lock()
	defer cl.Unlock()
	c, err := ReadBackendConfig(file, opts...)
	if err != nil {
		return fmt.Errorf("cannot read config file: %w", err)
	}

	if c.Validate() {
		cl.configs[c.Name] = *c
	} else {
		return fmt.Errorf("config is not valid")
	}

	return nil
}

func (cm *BackendConfigLoader) LoadBackendConfigsFromPath(path string, opts ...ConfigLoaderOption) error {
	cm.Lock()
	defer cm.Unlock()
	entries, err := os.ReadDir(path)
	if err != nil {
		return fmt.Errorf("cannot read directory '%s': %w", path, err)
	}
	files := make([]fs.FileInfo, 0, len(entries))
	for _, entry := range entries {
		info, err := entry.Info()
		if err != nil {
			return err
		}
		files = append(files, info)
	}
	for _, file := range files {
		// Skip templates, YAML and .keep files
		if !strings.Contains(file.Name(), ".yaml") && !strings.Contains(file.Name(), ".yml") ||
			strings.HasPrefix(file.Name(), ".") {
			continue
		}
		c, err := ReadBackendConfig(filepath.Join(path, file.Name()), opts...)
		if err != nil {
			log.Error().Err(err).Msgf("cannot read config file: %s", file.Name())
			continue
		}
		if c.Validate() {
			cm.configs[c.Name] = *c
		} else {
			log.Error().Err(err).Msgf("config is not valid")
		}
	}

	return nil
}

--- a/core/config/backend_config_loader.go
+++ b/core/config/backend_config_loader.go
@@ -7,7 +7,9 @@
 	}

 	for _, cc := range c {
-		cm.configs[cc.Name] = *cc
+		if cc.Validate() {
+			cm.configs[cc.Name] = *cc
+		}
 	}
 	return nil
 }

--- a/core/config/backend_config_loader.go
+++ b/core/config/backend_config_loader.go
@@ -6,6 +6,11 @@
 		return fmt.Errorf("cannot read config file: %w", err)
 	}

-	cl.configs[c.Name] = *c
+	if c.Validate() {
+		cl.configs[c.Name] = *c
+	} else {
+		return fmt.Errorf("config is not valid")
+	}
+
 	return nil
 }

--- a/core/config/backend_config_loader.go
+++ b/core/config/backend_config_loader.go
@@ -3,7 +3,7 @@
 	defer cm.Unlock()
 	entries, err := os.ReadDir(path)
 	if err != nil {
-		return err
+		return fmt.Errorf("cannot read directory '%s': %w", path, err)
 	}
 	files := make([]fs.FileInfo, 0, len(entries))
 	for _, entry := range entries {
@@ -20,8 +20,14 @@
 			continue
 		}
 		c, err := ReadBackendConfig(filepath.Join(path, file.Name()), opts...)
-		if err == nil {
+		if err != nil {
+			log.Error().Err(err).Msgf("cannot read config file: %s", file.Name())
+			continue
+		}
+		if c.Validate() {
 			cm.configs[c.Name] = *c
+		} else {
+			log.Error().Err(err).Msgf("config is not valid")
 		}
 	}


Contour is a Kubernetes ingress controller using Envoy proxy. In Contour before version 1.17.1 a specially crafted ExternalName type Service may be used to access Envoy's admin interface, which Contour normally prevents from access outside the Envoy container. This can be used to shut down Envoy remotely (a denial of service), or to expose the existence of any Secret that Envoy is using for its configuration, including most notably TLS Keypairs. However, it *cannot* be used to get the *content* of those secrets. Since this attack allows access to the administration interface, a variety of administration options are available, such as shutting down the Envoy or draining traffic. In general, the Envoy admin interface cannot easily be used for making changes to the cluster, in-flight requests, or backend services, but it could be used to shut down or drain Envoy, change traffic routing, or to retrieve secret metadata, as mentioned above. The issue will be addressed in Contour v1.18.0 and a cherry-picked patch release, v1.17.1, has been released to cover users who cannot upgrade at this time. For more details refer to the linked GitHub Security Advisory.

Externally Controlled Reference to a Resource in Another Sphere

The product uses an externally controlled name or reference that resolves to a resource that is outside of the intended control sphere.

func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache) (*Service, error) {
	svc, svcPort, err := cache.LookupService(meta, port)
	if err != nil {
		return nil, err
	}

	if dagSvc := dag.GetService(k8s.NamespacedNameOf(svc), svcPort.Port); dagSvc != nil {
		return dagSvc, nil
	}

	dagSvc := &Service{
		Weighted: WeightedService{
			ServiceName:      svc.Name,
			ServiceNamespace: svc.Namespace,
			ServicePort:      svcPort,
			Weight:           1,
		},
		Protocol:           upstreamProtocol(svc, svcPort),
		MaxConnections:     annotation.MaxConnections(svc),
		MaxPendingRequests: annotation.MaxPendingRequests(svc),
		MaxRequests:        annotation.MaxRequests(svc),
		MaxRetries:         annotation.MaxRetries(svc),
		ExternalName:       externalName(svc),
	}
	return dagSvc, nil
}

func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache, enableExternalNameSvc bool) (*Service, error) {
	svc, svcPort, err := cache.LookupService(meta, port)
	if err != nil {
		return nil, err
	}

	err = validateExternalName(svc, enableExternalNameSvc)
	if err != nil {
		return nil, err
	}

	if dagSvc := dag.GetService(k8s.NamespacedNameOf(svc), svcPort.Port); dagSvc != nil {
		return dagSvc, nil
	}

	dagSvc := &Service{
		Weighted: WeightedService{
			ServiceName:      svc.Name,
			ServiceNamespace: svc.Namespace,
			ServicePort:      svcPort,
			Weight:           1,
		},
		Protocol:           upstreamProtocol(svc, svcPort),
		MaxConnections:     annotation.MaxConnections(svc),
		MaxPendingRequests: annotation.MaxPendingRequests(svc),
		MaxRequests:        annotation.MaxRequests(svc),
		MaxRetries:         annotation.MaxRetries(svc),
		ExternalName:       externalName(svc),
	}
	return dagSvc, nil
}

func validateExternalName(svc *v1.Service, enableExternalNameSvc bool) error {

	// If this isn't an ExternalName Service, we're all good here.
	en := externalName(svc)
	if en == "" {
		return nil
	}

	// If ExternalNames are disabled, then we don't want to add this to the DAG.
	if !enableExternalNameSvc {
		return fmt.Errorf("%s/%s is an ExternalName service, these are not currently enabled. See the config.enableExternalNameService config file setting", svc.Namespace, svc.Name)
	}

	// Check against a list of known localhost names, using a map to approximate a set.
	// TODO(youngnick) This is a very porous hack, and we should probably look into doing a DNS
	// lookup to check what the externalName resolves to, but I'm worried about the
	// performance impact of doing one or more DNS lookups per DAG run, so we're
	// going to go with a specific blocklist for now.
	localhostNames := map[string]struct{}{
		"localhost":               {},
		"localhost.localdomain":   {},
		"local.projectcontour.io": {},
	}

	_, localhost := localhostNames[en]
	if localhost {
		return fmt.Errorf("%s/%s is an ExternalName service that points to localhost, this is not allowed", svc.Namespace, svc.Name)
	}

	return nil
}

--- a/internal/dag/accessors.go
+++ b/internal/dag/accessors.go
@@ -1,5 +1,10 @@
-func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache) (*Service, error) {
+func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache, enableExternalNameSvc bool) (*Service, error) {
 	svc, svcPort, err := cache.LookupService(meta, port)
+	if err != nil {
+		return nil, err
+	}
+
+	err = validateExternalName(svc, enableExternalNameSvc)
 	if err != nil {
 		return nil, err
 	}

--- /dev/null
+++ b/internal/dag/accessors.go
@@ -0,0 +1,31 @@
+func validateExternalName(svc *v1.Service, enableExternalNameSvc bool) error {
+
+	// If this isn't an ExternalName Service, we're all good here.
+	en := externalName(svc)
+	if en == "" {
+		return nil
+	}
+
+	// If ExternalNames are disabled, then we don't want to add this to the DAG.
+	if !enableExternalNameSvc {
+		return fmt.Errorf("%s/%s is an ExternalName service, these are not currently enabled. See the config.enableExternalNameService config file setting", svc.Namespace, svc.Name)
+	}
+
+	// Check against a list of known localhost names, using a map to approximate a set.
+	// TODO(youngnick) This is a very porous hack, and we should probably look into doing a DNS
+	// lookup to check what the externalName resolves to, but I'm worried about the
+	// performance impact of doing one or more DNS lookups per DAG run, so we're
+	// going to go with a specific blocklist for now.
+	localhostNames := map[string]struct{}{
+		"localhost":               {},
+		"localhost.localdomain":   {},
+		"local.projectcontour.io": {},
+	}
+
+	_, localhost := localhostNames[en]
+	if localhost {
+		return fmt.Errorf("%s/%s is an ExternalName service that points to localhost, this is not allowed", svc.Namespace, svc.Name)
+	}
+
+	return nil
+}

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2021-32783:latest
# bash /workspace/fix-run.sh
set -e

cd /workspace/contour
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
go test  -timeout 30s -run ^TestBuilderLookupService$ github.com/projectcontour/contour/internal/dag