go-jose before 1.0.4 suffers from multiple signatures exploitation. The go-jose library supports messages with multiple signatures. However, when validating a signed message the API did not indicate which signature was valid, which could potentially lead to confusion. For example, users of the library might mistakenly read protected header values from an attached signature that was different from the one originally validated.

Improper Access Control

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

import (
	"crypto/ecdsa"
	"crypto/rsa"
	"fmt"
	"reflect"
)

func (obj JsonWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) {
	headers := obj.mergedHeaders(nil)

	if len(headers.Crit) > 0 {
		return nil, fmt.Errorf("square/go-jose: unsupported crit header")
	}

	decrypter, err := newDecrypter(decryptionKey)
	if err != nil {
		return nil, err
	}

	cipher := getContentCipher(headers.Enc)
	if cipher == nil {
		return nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(headers.Enc))
	}

	generator := randomKeyGenerator{
		size: cipher.keySize(),
	}

	parts := &aeadParts{
		iv:         obj.iv,
		ciphertext: obj.ciphertext,
		tag:        obj.tag,
	}

	authData := obj.computeAuthData()

	var plaintext []byte
	for _, recipient := range obj.recipients {
		recipientHeaders := obj.mergedHeaders(&recipient)

		cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator)
		if err == nil {
			// Found a valid CEK -- let's try to decrypt.
			plaintext, err = cipher.decrypt(cek, authData, parts)
			if err == nil {
				break
			}
		}
	}

	if plaintext == nil {
		return nil, ErrCryptoFailure
	}

	// The "zip" header parameter may only be present in the protected header.
	if obj.protected.Zip != "" {
		plaintext, err = decompress(obj.protected.Zip, plaintext)
	}

	return plaintext, err
}

func (obj JsonWebSignature) Verify(verificationKey interface{}) ([]byte, error) {
	verifier, err := newVerifier(verificationKey)
	if err != nil {
		return nil, err
	}

	for _, signature := range obj.Signatures {
		headers := signature.mergedHeaders()
		if len(headers.Crit) > 0 {
			// Unsupported crit header
			continue
		}

		input := obj.computeAuthData(&signature)
		alg := SignatureAlgorithm(headers.Alg)
		err := verifier.verifyPayload(input, signature.Signature, alg)
		if err == nil {
			return obj.payload, nil
		}
	}

	return nil, ErrCryptoFailure
}

import (
	"crypto/ecdsa"
	"crypto/rsa"
	"errors"
	"fmt"
	"reflect"
)

func (obj JsonWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) {
	headers := obj.mergedHeaders(nil)

	if len(obj.recipients) > 1 {
		return nil, errors.New("square/go-jose: too many recipients in payload; expecting only one")
	}

	if len(headers.Crit) > 0 {
		return nil, fmt.Errorf("square/go-jose: unsupported crit header")
	}

	decrypter, err := newDecrypter(decryptionKey)
	if err != nil {
		return nil, err
	}

	cipher := getContentCipher(headers.Enc)
	if cipher == nil {
		return nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(headers.Enc))
	}

	generator := randomKeyGenerator{
		size: cipher.keySize(),
	}

	parts := &aeadParts{
		iv:         obj.iv,
		ciphertext: obj.ciphertext,
		tag:        obj.tag,
	}

	authData := obj.computeAuthData()

	var plaintext []byte
	recipient := obj.recipients[0]
	recipientHeaders := obj.mergedHeaders(&recipient)

	cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator)
	if err == nil {
		// Found a valid CEK -- let's try to decrypt.
		plaintext, err = cipher.decrypt(cek, authData, parts)
	}

	if plaintext == nil {
		return nil, ErrCryptoFailure
	}

	// The "zip" header parameter may only be present in the protected header.
	if obj.protected.Zip != "" {
		plaintext, err = decompress(obj.protected.Zip, plaintext)
	}

	return plaintext, err
}

// DecryptMulti decrypts and validates the object and returns the plaintexts,
// with support for multiple recipients. It returns the index of the recipient
// for which the decryption was successful, the merged headers for that recipient,
// and the plaintext.
func (obj JsonWebEncryption) DecryptMulti(decryptionKey interface{}) (int, JoseHeader, []byte, error) {
	globalHeaders := obj.mergedHeaders(nil)

	if len(globalHeaders.Crit) > 0 {
		return -1, JoseHeader{}, nil, fmt.Errorf("square/go-jose: unsupported crit header")
	}

	decrypter, err := newDecrypter(decryptionKey)
	if err != nil {
		return -1, JoseHeader{}, nil, err
	}

	cipher := getContentCipher(globalHeaders.Enc)
	if cipher == nil {
		return -1, JoseHeader{}, nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(globalHeaders.Enc))
	}

	generator := randomKeyGenerator{
		size: cipher.keySize(),
	}

	parts := &aeadParts{
		iv:         obj.iv,
		ciphertext: obj.ciphertext,
		tag:        obj.tag,
	}

	authData := obj.computeAuthData()

	index := -1
	var plaintext []byte
	var headers rawHeader

	for i, recipient := range obj.recipients {
		recipientHeaders := obj.mergedHeaders(&recipient)

		cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator)
		if err == nil {
			// Found a valid CEK -- let's try to decrypt.
			plaintext, err = cipher.decrypt(cek, authData, parts)
			if err == nil {
				index = i
				headers = recipientHeaders
				break
			}
		}
	}

	if plaintext == nil || err != nil {
		return -1, JoseHeader{}, nil, ErrCryptoFailure
	}

	// The "zip" header parameter may only be present in the protected header.
	if obj.protected.Zip != "" {
		plaintext, err = decompress(obj.protected.Zip, plaintext)
	}

	return index, headers.sanitized(), plaintext, err
}

// Verify validates the signature on the object and returns the payload.
// Note that this function does not support multi-signature, if you desire
// multi-sig verification use VerifyMulti instead.
func (obj JsonWebSignature) Verify(verificationKey interface{}) ([]byte, error) {
	verifier, err := newVerifier(verificationKey)
	if err != nil {
		return nil, err
	}

	if len(obj.Signatures) > 1 {
		return nil, errors.New("square/go-jose: too many signatures in payload; expecting only one")
	}

	signature := obj.Signatures[0]
	headers := signature.mergedHeaders()
	if len(headers.Crit) > 0 {
		// Unsupported crit header
		return nil, ErrCryptoFailure
	}

	input := obj.computeAuthData(&signature)
	alg := SignatureAlgorithm(headers.Alg)
	err = verifier.verifyPayload(input, signature.Signature, alg)
	if err == nil {
		return obj.payload, nil
	}

	return nil, ErrCryptoFailure
}

func (obj JsonWebSignature) VerifyMulti(verificationKey interface{}) (int, Signature, []byte, error) {
	verifier, err := newVerifier(verificationKey)
	if err != nil {
		return -1, Signature{}, nil, err
	}

	for i, signature := range obj.Signatures {
		headers := signature.mergedHeaders()
		if len(headers.Crit) > 0 {
			// Unsupported crit header
			continue
		}

		input := obj.computeAuthData(&signature)
		alg := SignatureAlgorithm(headers.Alg)
		err := verifier.verifyPayload(input, signature.Signature, alg)
		if err == nil {
			return i, signature, obj.payload, nil
		}
	}

	return -1, Signature{}, nil, ErrCryptoFailure
}

--- a/crypter.go
+++ b/crypter.go
@@ -1,6 +1,7 @@
 import (
 	"crypto/ecdsa"
 	"crypto/rsa"
+	"errors"
 	"fmt"
 	"reflect"
 )

--- a/crypter.go
+++ b/crypter.go
@@ -1,5 +1,9 @@
 func (obj JsonWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) {
 	headers := obj.mergedHeaders(nil)
+
+	if len(obj.recipients) > 1 {
+		return nil, errors.New("square/go-jose: too many recipients in payload; expecting only one")
+	}

 	if len(headers.Crit) > 0 {
 		return nil, fmt.Errorf("square/go-jose: unsupported crit header")
@@ -28,17 +32,13 @@
 	authData := obj.computeAuthData()

 	var plaintext []byte
-	for _, recipient := range obj.recipients {
-		recipientHeaders := obj.mergedHeaders(&recipient)
+	recipient := obj.recipients[0]
+	recipientHeaders := obj.mergedHeaders(&recipient)

-		cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator)
-		if err == nil {
-			// Found a valid CEK -- let's try to decrypt.
-			plaintext, err = cipher.decrypt(cek, authData, parts)
-			if err == nil {
-				break
-			}
-		}
+	cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator)
+	if err == nil {
+		// Found a valid CEK -- let's try to decrypt.
+		plaintext, err = cipher.decrypt(cek, authData, parts)
 	}

 	if plaintext == nil {
@@ -52,3 +52,67 @@

 	return plaintext, err
 }
+
+// DecryptMulti decrypts and validates the object and returns the plaintexts,
+// with support for multiple recipients. It returns the index of the recipient
+// for which the decryption was successful, the merged headers for that recipient,
+// and the plaintext.
+func (obj JsonWebEncryption) DecryptMulti(decryptionKey interface{}) (int, JoseHeader, []byte, error) {
+	globalHeaders := obj.mergedHeaders(nil)
+
+	if len(globalHeaders.Crit) > 0 {
+		return -1, JoseHeader{}, nil, fmt.Errorf("square/go-jose: unsupported crit header")
+	}
+
+	decrypter, err := newDecrypter(decryptionKey)
+	if err != nil {
+		return -1, JoseHeader{}, nil, err
+	}
+
+	cipher := getContentCipher(globalHeaders.Enc)
+	if cipher == nil {
+		return -1, JoseHeader{}, nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(globalHeaders.Enc))
+	}
+
+	generator := randomKeyGenerator{
+		size: cipher.keySize(),
+	}
+
+	parts := &aeadParts{
+		iv:         obj.iv,
+		ciphertext: obj.ciphertext,
+		tag:        obj.tag,
+	}
+
+	authData := obj.computeAuthData()
+
+	index := -1
+	var plaintext []byte
+	var headers rawHeader
+
+	for i, recipient := range obj.recipients {
+		recipientHeaders := obj.mergedHeaders(&recipient)
+
+		cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator)
+		if err == nil {
+			// Found a valid CEK -- let's try to decrypt.
+			plaintext, err = cipher.decrypt(cek, authData, parts)
+			if err == nil {
+				index = i
+				headers = recipientHeaders
+				break
+			}
+		}
+	}
+
+	if plaintext == nil || err != nil {
+		return -1, JoseHeader{}, nil, ErrCryptoFailure
+	}
+
+	// The "zip" header parameter may only be present in the protected header.
+	if obj.protected.Zip != "" {
+		plaintext, err = decompress(obj.protected.Zip, plaintext)
+	}
+
+	return index, headers.sanitized(), plaintext, err
+}

--- a/signing.go
+++ b/signing.go
@@ -1,22 +1,28 @@
+// Verify validates the signature on the object and returns the payload.
+// Note that this function does not support multi-signature, if you desire
+// multi-sig verification use VerifyMulti instead.
 func (obj JsonWebSignature) Verify(verificationKey interface{}) ([]byte, error) {
 	verifier, err := newVerifier(verificationKey)
 	if err != nil {
 		return nil, err
 	}

-	for _, signature := range obj.Signatures {
-		headers := signature.mergedHeaders()
-		if len(headers.Crit) > 0 {
-			// Unsupported crit header
-			continue
-		}
+	if len(obj.Signatures) > 1 {
+		return nil, errors.New("square/go-jose: too many signatures in payload; expecting only one")
+	}

-		input := obj.computeAuthData(&signature)
-		alg := SignatureAlgorithm(headers.Alg)
-		err := verifier.verifyPayload(input, signature.Signature, alg)
-		if err == nil {
-			return obj.payload, nil
-		}
+	signature := obj.Signatures[0]
+	headers := signature.mergedHeaders()
+	if len(headers.Crit) > 0 {
+		// Unsupported crit header
+		return nil, ErrCryptoFailure
+	}
+
+	input := obj.computeAuthData(&signature)
+	alg := SignatureAlgorithm(headers.Alg)
+	err = verifier.verifyPayload(input, signature.Signature, alg)
+	if err == nil {
+		return obj.payload, nil
 	}

 	return nil, ErrCryptoFailure

--- /dev/null
+++ b/signing.go
@@ -0,0 +1,23 @@
+func (obj JsonWebSignature) VerifyMulti(verificationKey interface{}) (int, Signature, []byte, error) {
+	verifier, err := newVerifier(verificationKey)
+	if err != nil {
+		return -1, Signature{}, nil, err
+	}
+
+	for i, signature := range obj.Signatures {
+		headers := signature.mergedHeaders()
+		if len(headers.Crit) > 0 {
+			// Unsupported crit header
+			continue
+		}
+
+		input := obj.computeAuthData(&signature)
+		alg := SignatureAlgorithm(headers.Alg)
+		err := verifier.verifyPayload(input, signature.Signature, alg)
+		if err == nil {
+			return i, signature, obj.payload, nil
+		}
+	}
+
+	return -1, Signature{}, nil, ErrCryptoFailure
+}


Astropy is a project for astronomy in Python that fosters interoperability between Python astronomy packages. Version 5.3.2 of the Astropy core package is vulnerable to remote code execution due to improper input validation in the `TranformGraph().to_dot_graph` function. A malicious user can provide a command or a script file as a value to the `savelayout` argument, which will be placed as the first value in a list of arguments passed to `subprocess.Popen`.  Although an error will be raised, the command or script will be executed successfully. Version 5.3.3 fixes this issue.

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 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.

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 to_dot_graph(
        self,
        priorities=True,
        addnodes=[],
        savefn=None,
        savelayout="plain",
        saveformat=None,
        color_edges=True,
    ):
        """
        Converts this transform graph to the graphviz_ DOT format.

        Optionally saves it (requires `graphviz`_ be installed and on your path).

        .. _graphviz: http://www.graphviz.org/

        Parameters
        ----------
        priorities : bool
            If `True`, show the priority values for each transform.  Otherwise,
            the will not be included in the graph.
        addnodes : sequence of str
            Additional coordinate systems to add (this can include systems
            already in the transform graph, but they will only appear once).
        savefn : None or str
            The file name to save this graph to or `None` to not save
            to a file.
        savelayout : str
            The graphviz program to use to layout the graph (see
            graphviz_ for details) or 'plain' to just save the DOT graph
            content. Ignored if ``savefn`` is `None`.
        saveformat : str
            The graphviz output format. (e.g. the ``-Txxx`` option for
            the command line program - see graphviz docs for details).
            Ignored if ``savefn`` is `None`.
        color_edges : bool
            Color the edges between two nodes (frames) based on the type of
            transform. ``FunctionTransform``: red, ``StaticMatrixTransform``:
            blue, ``DynamicMatrixTransform``: green.

        Returns
        -------
        dotgraph : str
            A string with the DOT format graph.
        """
        nodes = []
        # find the node names
        for a in self._graph:
            if a not in nodes:
                nodes.append(a)
            for b in self._graph[a]:
                if b not in nodes:
                    nodes.append(b)
        for node in addnodes:
            if node not in nodes:
                nodes.append(node)
        nodenames = []
        invclsaliases = {
            f: [k for k, v in self._cached_names.items() if v == f]
            for f in self.frame_set
        }
        for n in nodes:
            if n in invclsaliases:
                aliases = "`\\n`".join(invclsaliases[n])
                nodenames.append(
                    '{0} [shape=oval label="{0}\\n`{1}`"]'.format(n.__name__, aliases)
                )
            else:
                nodenames.append(n.__name__ + "[ shape=oval ]")

        edgenames = []
        # Now the edges
        for a in self._graph:
            agraph = self._graph[a]
            for b in agraph:
                transform = agraph[b]
                pri = transform.priority if hasattr(transform, "priority") else 1
                color = trans_to_color[transform.__class__] if color_edges else "black"
                edgenames.append((a.__name__, b.__name__, pri, color))

        # generate simple dot format graph
        lines = ["digraph AstropyCoordinateTransformGraph {"]
        lines.append("graph [rankdir=LR]")
        lines.append("; ".join(nodenames) + ";")
        for enm1, enm2, weights, color in edgenames:
            labelstr_fmt = "[ {0} {1} ]"

            if priorities:
                priority_part = f'label = "{weights}"'
            else:
                priority_part = ""

            color_part = f'color = "{color}"'

            labelstr = labelstr_fmt.format(priority_part, color_part)
            lines.append(f"{enm1} -> {enm2}{labelstr};")

        lines.append("")
        lines.append("overlap=false")
        lines.append("}")
        dotgraph = "\n".join(lines)

        if savefn is not None:
            if savelayout == "plain":
                with open(savefn, "w") as f:
                    f.write(dotgraph)
            else:
                args = [savelayout]
                if saveformat is not None:
                    args.append("-T" + saveformat)
                proc = subprocess.Popen(
                    args,
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                )
                stdout, stderr = proc.communicate(dotgraph)
                if proc.returncode != 0:
                    raise OSError("problem running graphviz: \n" + stderr)

                with open(savefn, "w") as f:
                    f.write(stdout)

        return dotgraph

    def to_dot_graph(
        self,
        priorities=True,
        addnodes=[],
        savefn=None,
        savelayout="plain",
        saveformat=None,
        color_edges=True,
    ):
        """
        Converts this transform graph to the graphviz_ DOT format.

        Optionally saves it (requires `graphviz`_ be installed and on your path).

        .. _graphviz: http://www.graphviz.org/

        Parameters
        ----------
        priorities : bool
            If `True`, show the priority values for each transform.  Otherwise,
            the will not be included in the graph.
        addnodes : sequence of str
            Additional coordinate systems to add (this can include systems
            already in the transform graph, but they will only appear once).
        savefn : None or str
            The file name to save this graph to or `None` to not save
            to a file.
        savelayout : {"plain", "dot", "neato", "fdp", "sfdp", "circo", "twopi", "nop", "nop2", "osage", "patchwork"}
            The graphviz program to use to layout the graph (see
            graphviz_ for details) or 'plain' to just save the DOT graph
            content. Ignored if ``savefn`` is `None`.
        saveformat : str
            The graphviz output format. (e.g. the ``-Txxx`` option for
            the command line program - see graphviz docs for details).
            Ignored if ``savefn`` is `None`.
        color_edges : bool
            Color the edges between two nodes (frames) based on the type of
            transform. ``FunctionTransform``: red, ``StaticMatrixTransform``:
            blue, ``DynamicMatrixTransform``: green.

        Returns
        -------
        dotgraph : str
            A string with the DOT format graph.
        """
        nodes = []
        # find the node names
        for a in self._graph:
            if a not in nodes:
                nodes.append(a)
            for b in self._graph[a]:
                if b not in nodes:
                    nodes.append(b)
        for node in addnodes:
            if node not in nodes:
                nodes.append(node)
        nodenames = []
        invclsaliases = {
            f: [k for k, v in self._cached_names.items() if v == f]
            for f in self.frame_set
        }
        for n in nodes:
            if n in invclsaliases:
                aliases = "`\\n`".join(invclsaliases[n])
                nodenames.append(
                    '{0} [shape=oval label="{0}\\n`{1}`"]'.format(n.__name__, aliases)
                )
            else:
                nodenames.append(n.__name__ + "[ shape=oval ]")

        edgenames = []
        # Now the edges
        for a in self._graph:
            agraph = self._graph[a]
            for b in agraph:
                transform = agraph[b]
                pri = transform.priority if hasattr(transform, "priority") else 1
                color = trans_to_color[transform.__class__] if color_edges else "black"
                edgenames.append((a.__name__, b.__name__, pri, color))

        # generate simple dot format graph
        lines = ["digraph AstropyCoordinateTransformGraph {"]
        lines.append("graph [rankdir=LR]")
        lines.append("; ".join(nodenames) + ";")
        for enm1, enm2, weights, color in edgenames:
            labelstr_fmt = "[ {0} {1} ]"

            if priorities:
                priority_part = f'label = "{weights}"'
            else:
                priority_part = ""

            color_part = f'color = "{color}"'

            labelstr = labelstr_fmt.format(priority_part, color_part)
            lines.append(f"{enm1} -> {enm2}{labelstr};")

        lines.append("")
        lines.append("overlap=false")
        lines.append("}")
        dotgraph = "\n".join(lines)

        if savefn is not None:
            if savelayout == "plain":
                with open(savefn, "w") as f:
                    f.write(dotgraph)
            # Options from https://graphviz.org/docs/layouts/
            elif savelayout in (
                "dot",
                "neato",
                "fdp",
                "sfdp",
                "circo",
                "twopi",
                "nop",
                "nop2",
                "osage",
                "patchwork",
            ):
                args = [savelayout]
                if saveformat is not None:
                    args.append("-T" + saveformat)
                proc = subprocess.Popen(
                    args,
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                )
                stdout, stderr = proc.communicate(dotgraph)
                if proc.returncode != 0:
                    raise OSError("problem running graphviz: \n" + stderr)

                with open(savefn, "w") as f:
                    f.write(stdout)
            else:
                raise NotImplementedError(f'savelayout="{savelayout}" is not supported')

        return dotgraph

--- a/astropy/coordinates/transformations.py
+++ b/astropy/coordinates/transformations.py
@@ -25,7 +25,7 @@
         savefn : None or str
             The file name to save this graph to or `None` to not save
             to a file.
-        savelayout : str
+        savelayout : {"plain", "dot", "neato", "fdp", "sfdp", "circo", "twopi", "nop", "nop2", "osage", "patchwork"}
             The graphviz program to use to layout the graph (see
             graphviz_ for details) or 'plain' to just save the DOT graph
             content. Ignored if ``savefn`` is `None`.
@@ -104,7 +104,19 @@
             if savelayout == "plain":
                 with open(savefn, "w") as f:
                     f.write(dotgraph)
-            else:
+            # Options from https://graphviz.org/docs/layouts/
+            elif savelayout in (
+                "dot",
+                "neato",
+                "fdp",
+                "sfdp",
+                "circo",
+                "twopi",
+                "nop",
+                "nop2",
+                "osage",
+                "patchwork",
+            ):
                 args = [savelayout]
                 if saveformat is not None:
                     args.append("-T" + saveformat)
@@ -120,5 +132,7 @@

                 with open(savefn, "w") as f:
                     f.write(stdout)
+            else:
+                raise NotImplementedError(f'savelayout="{savelayout}" is not supported')

         return dotgraph


Tailscale is software for using Wireguard and multi-factor authentication (MFA). A vulnerability identified in the implementation of Tailscale SSH starting in version 1.34.0 and prior to prior to 1.38.2 in FreeBSD allows commands to be run with a higher privilege group ID than that specified in Tailscale SSH access rules. A difference in the behavior of the FreeBSD `setgroups` system call from POSIX meant that the Tailscale client running on a FreeBSD-based operating system did not appropriately restrict groups on the host when using Tailscale SSH. When accessing a FreeBSD host over Tailscale SSH, the egid of the tailscaled process was used instead of that of the user specified in Tailscale SSH access rules.

Tailscale SSH commands may have been run with a higher privilege group ID than that specified in Tailscale SSH access rules if they met all of the following criteria: the destination node was a FreeBSD device with Tailscale SSH enabled; Tailscale SSH access rules permitted access for non-root users; and a non-interactive SSH session was used.

Affected users should upgrade to version 1.38.2 to remediate the 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 beIncubator(args []string) error {
	ia := parseIncubatorArgs(args)
	if ia.isSFTP && ia.isShell {
		return fmt.Errorf("--sftp and --shell are mutually exclusive")
	}

	logf := logger.Discard
	if debugIncubator {
		// We don't own stdout or stderr, so the only place we can log is syslog.
		if sl, err := syslog.New(syslog.LOG_INFO|syslog.LOG_DAEMON, "tailscaled-ssh"); err == nil {
			logf = log.New(sl, "", 0).Printf
		}
	}

	euid := uint64(os.Geteuid())
	runningAsRoot := euid == 0
	if runningAsRoot && ia.loginCmdPath != "" {
		// Check if we can exec into the login command instead of trying to
		// incubate ourselves.
		if la := ia.loginArgs(); la != nil {
			return unix.Exec(ia.loginCmdPath, la, os.Environ())
		}
	}

	// Inform the system that we are about to log someone in.
	// We can only do this if we are running as root.
	// This is best effort to still allow running on machines where
	// we don't support starting sessions, e.g. darwin.
	sessionCloser, err := maybeStartLoginSession(logf, ia)
	if err == nil && sessionCloser != nil {
		defer sessionCloser()
	}
	var groupIDs []int
	for _, g := range strings.Split(ia.groups, ",") {
		gid, err := strconv.ParseInt(g, 10, 32)
		if err != nil {
			return err
		}
		groupIDs = append(groupIDs, int(gid))
	}

	if err := setGroups(groupIDs); err != nil {
		return err
	}
	if egid := os.Getegid(); egid != ia.gid {
		if err := syscall.Setgid(int(ia.gid)); err != nil {
			logf(err.Error())
			os.Exit(1)
		}
	}
	if euid != ia.uid {
		// Switch users if required before starting the desired process.
		if err := syscall.Setuid(int(ia.uid)); err != nil {
			logf(err.Error())
			os.Exit(1)
		}
	}
	if ia.isSFTP {
		logf("handling sftp")

		server, err := sftp.NewServer(stdRWC{})
		if err != nil {
			return err
		}
		return server.Serve()
	}

	cmd := exec.Command(ia.cmdName, ia.cmdArgs...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Env = os.Environ()

	if ia.hasTTY {
		// If we were launched with a tty then we should
		// mark that as the ctty of the child. However,
		// as the ctty is being passed from the parent
		// we set the child to foreground instead which
		// also passes the ctty.
		// However, we can not do this if never had a tty to
		// begin with.
		cmd.SysProcAttr = &syscall.SysProcAttr{
			Foreground: true,
		}
	}
	err = cmd.Run()
	if ee, ok := err.(*exec.ExitError); ok {
		ps := ee.ProcessState
		code := ps.ExitCode()
		if code < 0 {
			// TODO(bradfitz): do we need to also check the syscall.WaitStatus
			// and make our process look like it also died by signal/same signal
			// as our child process? For now we just do the exit code.
			fmt.Fprintf(os.Stderr, "[tailscale-ssh: process died: %v]\n", ps.String())
			code = 1 // for now. so we don't exit with negative
		}
		os.Exit(code)
	}
	return err
}

func beIncubator(args []string) error {
	ia := parseIncubatorArgs(args)
	if ia.isSFTP && ia.isShell {
		return fmt.Errorf("--sftp and --shell are mutually exclusive")
	}

	logf := logger.Discard
	if debugIncubator {
		// We don't own stdout or stderr, so the only place we can log is syslog.
		if sl, err := syslog.New(syslog.LOG_INFO|syslog.LOG_DAEMON, "tailscaled-ssh"); err == nil {
			logf = log.New(sl, "", 0).Printf
		}
	}

	euid := uint64(os.Geteuid())
	runningAsRoot := euid == 0
	if runningAsRoot && ia.loginCmdPath != "" {
		// Check if we can exec into the login command instead of trying to
		// incubate ourselves.
		if la := ia.loginArgs(); la != nil {
			return unix.Exec(ia.loginCmdPath, la, os.Environ())
		}
	}

	// Inform the system that we are about to log someone in.
	// We can only do this if we are running as root.
	// This is best effort to still allow running on machines where
	// we don't support starting sessions, e.g. darwin.
	sessionCloser, err := maybeStartLoginSession(logf, ia)
	if err == nil && sessionCloser != nil {
		defer sessionCloser()
	}

	var groupIDs []int
	for _, g := range strings.Split(ia.groups, ",") {
		gid, err := strconv.ParseInt(g, 10, 32)
		if err != nil {
			return err
		}
		groupIDs = append(groupIDs, int(gid))
	}

	if err := dropPrivileges(logf, int(ia.uid), ia.gid, groupIDs); err != nil {
		return err
	}

	if ia.isSFTP {
		logf("handling sftp")

		server, err := sftp.NewServer(stdRWC{})
		if err != nil {
			return err
		}
		return server.Serve()
	}

	cmd := exec.Command(ia.cmdName, ia.cmdArgs...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Env = os.Environ()

	if ia.hasTTY {
		// If we were launched with a tty then we should
		// mark that as the ctty of the child. However,
		// as the ctty is being passed from the parent
		// we set the child to foreground instead which
		// also passes the ctty.
		// However, we can not do this if never had a tty to
		// begin with.
		cmd.SysProcAttr = &syscall.SysProcAttr{
			Foreground: true,
		}
	}
	err = cmd.Run()
	if ee, ok := err.(*exec.ExitError); ok {
		ps := ee.ProcessState
		code := ps.ExitCode()
		if code < 0 {
			// TODO(bradfitz): do we need to also check the syscall.WaitStatus
			// and make our process look like it also died by signal/same signal
			// as our child process? For now we just do the exit code.
			fmt.Fprintf(os.Stderr, "[tailscale-ssh: process died: %v]\n", ps.String())
			code = 1 // for now. so we don't exit with negative
		}
		os.Exit(code)
	}
	return err
}

// TODO(andrew-d): verify that this works in more configurations before
// enabling by default.
const assertDropPrivileges = false

// dropPrivileges contains all the logic for dropping privileges to a different
// UID, GID, and set of supplementary groups. This function is
// security-sensitive and ordering-dependent; please be very cautious if/when
// refactoring.
//
// WARNING: if you change this function, you *MUST* run the TestDropPrivileges
// test in this package as root on at least Linux, FreeBSD and Darwin. This can
// be done by running:
//
//	go test -c ./ssh/tailssh/ && sudo ./tailssh.test -test.v -test.run TestDropPrivileges
func dropPrivileges(logf logger.Logf, wantUid, wantGid int, supplementaryGroups []int) error {
	fatalf := func(format string, args ...any) {
		logf(format, args...)
		os.Exit(1)
	}

	euid := os.Geteuid()
	egid := os.Getegid()

	if runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" {
		// On FreeBSD and Darwin, the first entry returned from the
		// getgroups(2) syscall is the egid, and changing it with
		// setgroups(2) changes the egid of the process. This is
		// technically a violation of the POSIX standard; see the
		// following article for more detail:
		//    https://www.usenix.org/system/files/login/articles/325-tsafrir.pdf
		//
		// In this case, we add an entry at the beginning of the
		// groupIDs list containing the expected gid if it's not
		// already there, which modifies the egid and additional groups
		// as one unit.
		if len(supplementaryGroups) == 0 || supplementaryGroups[0] != wantGid {
			supplementaryGroups = append([]int{wantGid}, supplementaryGroups...)
		}
	}

	if err := setGroups(supplementaryGroups); err != nil {
		return err
	}
	if egid != wantGid {
		// On FreeBSD and Darwin, we may have already called the
		// equivalent of setegid(wantGid) via the call to setGroups,
		// above. However, per the manpage, setgid(getegid()) is an
		// allowed operation regardless of privilege level.
		//
		// FreeBSD:
		//	The setgid() system call is permitted if the specified ID
		//	is equal to the real group ID or the effective group ID
		//	of the process, or if the effective user ID is that of
		//	the super user.
		//
		// Darwin:
		//	The setgid() function is permitted if the effective
		//	user ID is that of the super user, or if the specified
		//	group ID is the same as the effective group ID.  If
		//	not, but the specified group ID is the same as the real
		//	group ID, setgid() will set the effective group ID to
		//	the real group ID.
		if err := syscall.Setgid(wantGid); err != nil {
			fatalf("Setgid(%d): %v", wantGid, err)
		}
	}
	if euid != wantUid {
		// Switch users if required before starting the desired process.
		if err := syscall.Setuid(wantUid); err != nil {
			fatalf("Setuid(%d): %v", wantUid, err)
		}
	}

	// If we changed either the UID or GID, defensively assert that we
	// cannot reset the it back to our original values, and that the
	// current egid/euid are the expected values after we change
	// everything; if not, we exit the process.
	if assertDropPrivileges {
		if egid != wantGid {
			if err := syscall.Setegid(egid); err == nil {
				fatalf("unexpectedly able to set egid back to %d", egid)
			}
		}
		if euid != wantUid {
			if err := syscall.Seteuid(euid); err == nil {
				fatalf("unexpectedly able to set euid back to %d", euid)
			}
		}

		if got := os.Getegid(); got != wantGid {
			fatalf("got egid=%d, want %d", got, wantGid)
		}
		if got := os.Geteuid(); got != wantUid {
			fatalf("got euid=%d, want %d", got, wantUid)
		}

		// TODO(andrew-d): assert that our supplementary groups are correct
	}

	return nil
}

--- a/ssh/tailssh/incubator.go
+++ b/ssh/tailssh/incubator.go
@@ -30,6 +30,7 @@
 	if err == nil && sessionCloser != nil {
 		defer sessionCloser()
 	}
+
 	var groupIDs []int
 	for _, g := range strings.Split(ia.groups, ",") {
 		gid, err := strconv.ParseInt(g, 10, 32)
@@ -39,22 +40,10 @@
 		groupIDs = append(groupIDs, int(gid))
 	}

-	if err := setGroups(groupIDs); err != nil {
+	if err := dropPrivileges(logf, int(ia.uid), ia.gid, groupIDs); err != nil {
 		return err
 	}
-	if egid := os.Getegid(); egid != ia.gid {
-		if err := syscall.Setgid(int(ia.gid)); err != nil {
-			logf(err.Error())
-			os.Exit(1)
-		}
-	}
-	if euid != ia.uid {
-		// Switch users if required before starting the desired process.
-		if err := syscall.Setuid(int(ia.uid)); err != nil {
-			logf(err.Error())
-			os.Exit(1)
-		}
-	}
+
 	if ia.isSFTP {
 		logf("handling sftp")

--- /dev/null
+++ b/ssh/tailssh/incubator.go
@@ -0,0 +1,101 @@
+// TODO(andrew-d): verify that this works in more configurations before
+// enabling by default.
+const assertDropPrivileges = false
+
+// dropPrivileges contains all the logic for dropping privileges to a different
+// UID, GID, and set of supplementary groups. This function is
+// security-sensitive and ordering-dependent; please be very cautious if/when
+// refactoring.
+//
+// WARNING: if you change this function, you *MUST* run the TestDropPrivileges
+// test in this package as root on at least Linux, FreeBSD and Darwin. This can
+// be done by running:
+//
+//	go test -c ./ssh/tailssh/ && sudo ./tailssh.test -test.v -test.run TestDropPrivileges
+func dropPrivileges(logf logger.Logf, wantUid, wantGid int, supplementaryGroups []int) error {
+	fatalf := func(format string, args ...any) {
+		logf(format, args...)
+		os.Exit(1)
+	}
+
+	euid := os.Geteuid()
+	egid := os.Getegid()
+
+	if runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" {
+		// On FreeBSD and Darwin, the first entry returned from the
+		// getgroups(2) syscall is the egid, and changing it with
+		// setgroups(2) changes the egid of the process. This is
+		// technically a violation of the POSIX standard; see the
+		// following article for more detail:
+		//    https://www.usenix.org/system/files/login/articles/325-tsafrir.pdf
+		//
+		// In this case, we add an entry at the beginning of the
+		// groupIDs list containing the expected gid if it's not
+		// already there, which modifies the egid and additional groups
+		// as one unit.
+		if len(supplementaryGroups) == 0 || supplementaryGroups[0] != wantGid {
+			supplementaryGroups = append([]int{wantGid}, supplementaryGroups...)
+		}
+	}
+
+	if err := setGroups(supplementaryGroups); err != nil {
+		return err
+	}
+	if egid != wantGid {
+		// On FreeBSD and Darwin, we may have already called the
+		// equivalent of setegid(wantGid) via the call to setGroups,
+		// above. However, per the manpage, setgid(getegid()) is an
+		// allowed operation regardless of privilege level.
+		//
+		// FreeBSD:
+		//	The setgid() system call is permitted if the specified ID
+		//	is equal to the real group ID or the effective group ID
+		//	of the process, or if the effective user ID is that of
+		//	the super user.
+		//
+		// Darwin:
+		//	The setgid() function is permitted if the effective
+		//	user ID is that of the super user, or if the specified
+		//	group ID is the same as the effective group ID.  If
+		//	not, but the specified group ID is the same as the real
+		//	group ID, setgid() will set the effective group ID to
+		//	the real group ID.
+		if err := syscall.Setgid(wantGid); err != nil {
+			fatalf("Setgid(%d): %v", wantGid, err)
+		}
+	}
+	if euid != wantUid {
+		// Switch users if required before starting the desired process.
+		if err := syscall.Setuid(wantUid); err != nil {
+			fatalf("Setuid(%d): %v", wantUid, err)
+		}
+	}
+
+	// If we changed either the UID or GID, defensively assert that we
+	// cannot reset the it back to our original values, and that the
+	// current egid/euid are the expected values after we change
+	// everything; if not, we exit the process.
+	if assertDropPrivileges {
+		if egid != wantGid {
+			if err := syscall.Setegid(egid); err == nil {
+				fatalf("unexpectedly able to set egid back to %d", egid)
+			}
+		}
+		if euid != wantUid {
+			if err := syscall.Seteuid(euid); err == nil {
+				fatalf("unexpectedly able to set euid back to %d", euid)
+			}
+		}
+
+		if got := os.Getegid(); got != wantGid {
+			fatalf("got egid=%d, want %d", got, wantGid)
+		}
+		if got := os.Geteuid(); got != wantUid {
+			fatalf("got euid=%d, want %d", got, wantUid)
+		}
+
+		// TODO(andrew-d): assert that our supplementary groups are correct
+	}
+
+	return nil
+}


lib/utils.js in mquery before 3.2.3 allows a pollution attack because a special property (e.g., __proto__) can be copied during a merge or clone operation.

exports.cloneObject = function cloneObject(obj, options) {
  var minimize = options && options.minimize;
  var ret = {};
  var hasKeys;
  var val;
  var k;

  for (k in obj) {
    val = clone(obj[k], options);

    if (!minimize || ('undefined' !== typeof val)) {
      hasKeys || (hasKeys = true);
      ret[k] = val;
    }
  }

  return minimize
    ? hasKeys && ret
    : ret;
};

exports.merge = function merge(to, from) {
  var keys = Object.keys(from),
      i = keys.length,
      key;

  while (i--) {
    key = keys[i];
    if ('undefined' === typeof to[key]) {
      to[key] = from[key];
    } else {
      if (exports.isObject(from[key])) {
        merge(to[key], from[key]);
      } else {
        to[key] = from[key];
      }
    }
  }
};

exports.cloneObject = function cloneObject(obj, options) {
  var minimize = options && options.minimize;
  var ret = {};
  var hasKeys;
  var val;
  var k;

  for (k in obj) {
    // Not technically prototype pollution because this wouldn't merge properties
    // onto `Object.prototype`, but avoid properties like __proto__ as a precaution.
    if (specialProperties.indexOf(k) !== -1) {
      continue;
    }

    val = clone(obj[k], options);

    if (!minimize || ('undefined' !== typeof val)) {
      hasKeys || (hasKeys = true);
      ret[k] = val;
    }
  }

  return minimize
    ? hasKeys && ret
    : ret;
};

exports.merge = function merge(to, from) {
  var keys = Object.keys(from),
      i = keys.length,
      key;

  while (i--) {
    key = keys[i];
    if (specialProperties.indexOf(key) !== -1) {
      continue;
    }
    if ('undefined' === typeof to[key]) {
      to[key] = from[key];
    } else {
      if (exports.isObject(from[key])) {
        merge(to[key], from[key]);
      } else {
        to[key] = from[key];
      }
    }
  }
};

--- a/lib/utils.js
+++ b/lib/utils.js
@@ -1,20 +1 @@
-exports.cloneObject = function cloneObject(obj, options) {
-  var minimize = options && options.minimize;
-  var ret = {};
-  var hasKeys;
-  var val;
-  var k;
-
-  for (k in obj) {
-    val = clone(obj[k], options);
-
-    if (!minimize || ('undefined' !== typeof val)) {
-      hasKeys || (hasKeys = true);
-      ret[k] = val;
-    }
-  }
-
-  return minimize
-    ? hasKeys && ret
-    : ret;
-};
+var specialProperties = ['__proto__', 'constructor', 'prototype'];

--- a/lib/utils.js
+++ b/lib/utils.js
@@ -1,18 +1,26 @@
-exports.merge = function merge(to, from) {
-  var keys = Object.keys(from),
-      i = keys.length,
-      key;
+exports.cloneObject = function cloneObject(obj, options) {
+  var minimize = options && options.minimize;
+  var ret = {};
+  var hasKeys;
+  var val;
+  var k;

-  while (i--) {
-    key = keys[i];
-    if ('undefined' === typeof to[key]) {
-      to[key] = from[key];
-    } else {
-      if (exports.isObject(from[key])) {
-        merge(to[key], from[key]);
-      } else {
-        to[key] = from[key];
-      }
+  for (k in obj) {
+    // Not technically prototype pollution because this wouldn't merge properties
+    // onto `Object.prototype`, but avoid properties like __proto__ as a precaution.
+    if (specialProperties.indexOf(k) !== -1) {
+      continue;
+    }
+
+    val = clone(obj[k], options);
+
+    if (!minimize || ('undefined' !== typeof val)) {
+      hasKeys || (hasKeys = true);
+      ret[k] = val;
     }
   }
+
+  return minimize
+    ? hasKeys && ret
+    : ret;
 };

--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,21 @@
+exports.merge = function merge(to, from) {
+  var keys = Object.keys(from),
+      i = keys.length,
+      key;
+
+  while (i--) {
+    key = keys[i];
+    if (specialProperties.indexOf(key) !== -1) {
+      continue;
+    }
+    if ('undefined' === typeof to[key]) {
+      to[key] = from[key];
+    } else {
+      if (exports.isObject(from[key])) {
+        merge(to[key], from[key]);
+      } else {
+        to[key] = from[key];
+      }
+    }
+  }
+};


nodejs ejs version older than 2.5.5 is vulnerable to a denial-of-service due to weak input validation in the ejs.renderFile()

Improper Input Validation

The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.

function cpOptsInData(data, opts) {
  _OPTS.forEach(function (p) {
    if (typeof data[p] != 'undefined') {
      // Disallow setting the root opt for includes via a passed data obj
      // Unsanitized, parameterized use of `render` could allow the
      // include directory to be reset, opening up the possibility of
      // remote code execution
      if (p == 'root') {
        return;
      }
      opts[p] = data[p];
    }
  });
}

function cpOptsInData(data, opts) {
  _OPTS.forEach(function (p) {
    if (typeof data[p] != 'undefined') {
      // Disallow passing potentially dangerous opts in the data
      // These opts should not be settable via a `render` call
      if (_OPTS_IN_DATA_BLACKLIST[p]) {
        return;
      }
      opts[p] = data[p];
    }
  });
}

var _OPTS_IN_DATA_BLACKLIST = {
      cache: true,
      filename: true,
      root: true,
      localsName: true
    };

--- a/lib/ejs.js
+++ b/lib/ejs.js
@@ -1,11 +1,9 @@
 function cpOptsInData(data, opts) {
   _OPTS.forEach(function (p) {
     if (typeof data[p] != 'undefined') {
-      // Disallow setting the root opt for includes via a passed data obj
-      // Unsanitized, parameterized use of `render` could allow the
-      // include directory to be reset, opening up the possibility of
-      // remote code execution
-      if (p == 'root') {
+      // Disallow passing potentially dangerous opts in the data
+      // These opts should not be settable via a `render` call
+      if (_OPTS_IN_DATA_BLACKLIST[p]) {
         return;
       }
       opts[p] = data[p];

--- /dev/null
+++ b/lib/ejs.js
@@ -0,0 +1,6 @@
+var _OPTS_IN_DATA_BLACKLIST = {
+      cache: true,
+      filename: true,
+      root: true,
+      localsName: true
+    };

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

cd /workspace/ejs
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
npx mocha --grep "disallow unsafe opts passed along in data"

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

cd /workspace/ejs
git apply --whitespace=nowarn /workspace/fix.patch
npx mocha --timeout 3000 test/ejs.js --grep "whitespace slurp and rmWhitespace work" --invert


The V2 API in OpenStack Image Registry and Delivery Service (Glance) before 2014.1.4 and 2014.2.x before 2014.2.2 allows remote authenticated users to read or delete arbitrary files via a full pathname in a filesystem: URL in the image location property.  NOTE: this vulnerability exists because of an incomplete fix for CVE-2014-9493.

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.

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 validate_external_location(uri):
    """
    Validate if URI of external location are supported.

    Only over non-local store types are OK, i.e. S3, Swift,
    HTTP. Note the absence of 'file://' for security reasons,
    see LP bug #942118, 1400966, 'swift+config://' is also
    absent for security reasons, see LP bug #1334196.

    :param uri: The URI of external image location.
    :return: Whether given URI of external image location are OK.
    """

    # TODO(zhiyan): This function could be moved to glance_store.

    pieces = urlparse.urlparse(uri)
    valid_schemes = [scheme for scheme in store_api.get_known_schemes()
                     if scheme != 'file' and scheme != 'swift+config']
    return pieces.scheme in valid_schemes

def validate_external_location(uri):
    """
    Validate if URI of external location are supported.

    Only over non-local store types are OK, i.e. S3, Swift,
    HTTP. Note the absence of 'file://' for security reasons,
    see LP bug #942118, 1400966, 'swift+config://' is also
    absent for security reasons, see LP bug #1334196.

    :param uri: The URI of external image location.
    :return: Whether given URI of external image location are OK.
    """

    # TODO(zhiyan): This function could be moved to glance_store.
    # TODO(gm): Use a whitelist of allowed schemes
    scheme = urlparse.urlparse(uri).scheme
    return (scheme in store_api.get_known_schemes() and
            scheme not in RESTRICTED_URI_SCHEMAS)

--- a/glance/common/store_utils.py
+++ b/glance/common/store_utils.py
@@ -12,8 +12,7 @@
     """

     # TODO(zhiyan): This function could be moved to glance_store.
-
-    pieces = urlparse.urlparse(uri)
-    valid_schemes = [scheme for scheme in store_api.get_known_schemes()
-                     if scheme != 'file' and scheme != 'swift+config']
-    return pieces.scheme in valid_schemes
+    # TODO(gm): Use a whitelist of allowed schemes
+    scheme = urlparse.urlparse(uri).scheme
+    return (scheme in store_api.get_known_schemes() and
+            scheme not in RESTRICTED_URI_SCHEMAS)


The Kubernetes kube-apiserver in versions v1.6-v1.15, and versions prior to v1.16.13, v1.17.9 and v1.18.6 are vulnerable to an unvalidated redirect on proxied upgrade requests that could allow an attacker to escalate privileges from a node compromise to a full cluster compromise.

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.

func (h *UpgradeAwareHandler) tryUpgrade(w http.ResponseWriter, req *http.Request) bool {
	if !httpstream.IsUpgradeRequest(req) {
		klog.V(6).Infof("Request was not an upgrade")
		return false
	}

	var (
		backendConn net.Conn
		rawResponse []byte
		err         error
	)

	location := *h.Location
	if h.UseRequestLocation {
		location = *req.URL
		location.Scheme = h.Location.Scheme
		location.Host = h.Location.Host
	}

	clone := utilnet.CloneRequest(req)
	// Only append X-Forwarded-For in the upgrade path, since httputil.NewSingleHostReverseProxy
	// handles this in the non-upgrade path.
	utilnet.AppendForwardedForHeader(clone)
	if h.InterceptRedirects {
		klog.V(6).Infof("Connecting to backend proxy (intercepting redirects) %s\n  Headers: %v", &location, clone.Header)
		backendConn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, &location, clone.Header, req.Body, utilnet.DialerFunc(h.DialForUpgrade), h.RequireSameHostRedirects)
	} else {
		klog.V(6).Infof("Connecting to backend proxy (direct dial) %s\n  Headers: %v", &location, clone.Header)
		clone.URL = &location
		backendConn, err = h.DialForUpgrade(clone)
	}
	if err != nil {
		klog.V(6).Infof("Proxy connection error: %v", err)
		h.Responder.Error(w, req, err)
		return true
	}
	defer backendConn.Close()

	// determine the http response code from the backend by reading from rawResponse+backendConn
	backendHTTPResponse, headerBytes, err := getResponse(io.MultiReader(bytes.NewReader(rawResponse), backendConn))
	if err != nil {
		klog.V(6).Infof("Proxy connection error: %v", err)
		h.Responder.Error(w, req, err)
		return true
	}
	if len(headerBytes) > len(rawResponse) {
		// we read beyond the bytes stored in rawResponse, update rawResponse to the full set of bytes read from the backend
		rawResponse = headerBytes
	}

	// Once the connection is hijacked, the ErrorResponder will no longer work, so
	// hijacking should be the last step in the upgrade.
	requestHijacker, ok := w.(http.Hijacker)
	if !ok {
		klog.V(6).Infof("Unable to hijack response writer: %T", w)
		h.Responder.Error(w, req, fmt.Errorf("request connection cannot be hijacked: %T", w))
		return true
	}
	requestHijackedConn, _, err := requestHijacker.Hijack()
	if err != nil {
		klog.V(6).Infof("Unable to hijack response: %v", err)
		h.Responder.Error(w, req, fmt.Errorf("error hijacking connection: %v", err))
		return true
	}
	defer requestHijackedConn.Close()

	if backendHTTPResponse.StatusCode != http.StatusSwitchingProtocols {
		// If the backend did not upgrade the request, echo the response from the backend to the client and return, closing the connection.
		klog.V(6).Infof("Proxy upgrade error, status code %d", backendHTTPResponse.StatusCode)
		// set read/write deadlines
		deadline := time.Now().Add(10 * time.Second)
		backendConn.SetReadDeadline(deadline)
		requestHijackedConn.SetWriteDeadline(deadline)
		// write the response to the client
		err := backendHTTPResponse.Write(requestHijackedConn)
		if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
			klog.Errorf("Error proxying data from backend to client: %v", err)
		}
		// Indicate we handled the request
		return true
	}

	// Forward raw response bytes back to client.
	if len(rawResponse) > 0 {
		klog.V(6).Infof("Writing %d bytes to hijacked connection", len(rawResponse))
		if _, err = requestHijackedConn.Write(rawResponse); err != nil {
			utilruntime.HandleError(fmt.Errorf("Error proxying response from backend to client: %v", err))
		}
	}

	// Proxy the connection. This is bidirectional, so we need a goroutine
	// to copy in each direction. Once one side of the connection exits, we
	// exit the function which performs cleanup and in the process closes
	// the other half of the connection in the defer.
	writerComplete := make(chan struct{})
	readerComplete := make(chan struct{})

	go func() {
		var writer io.WriteCloser
		if h.MaxBytesPerSec > 0 {
			writer = flowrate.NewWriter(backendConn, h.MaxBytesPerSec)
		} else {
			writer = backendConn
		}
		_, err := io.Copy(writer, requestHijackedConn)
		if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
			klog.Errorf("Error proxying data from client to backend: %v", err)
		}
		close(writerComplete)
	}()

	go func() {
		var reader io.ReadCloser
		if h.MaxBytesPerSec > 0 {
			reader = flowrate.NewReader(backendConn, h.MaxBytesPerSec)
		} else {
			reader = backendConn
		}
		_, err := io.Copy(requestHijackedConn, reader)
		if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
			klog.Errorf("Error proxying data from backend to client: %v", err)
		}
		close(readerComplete)
	}()

	// Wait for one half the connection to exit. Once it does the defer will
	// clean up the other half of the connection.
	select {
	case <-writerComplete:
	case <-readerComplete:
	}
	klog.V(6).Infof("Disconnecting from backend proxy %s\n  Headers: %v", &location, clone.Header)

	return true
}

func (h *UpgradeAwareHandler) tryUpgrade(w http.ResponseWriter, req *http.Request) bool {
	if !httpstream.IsUpgradeRequest(req) {
		klog.V(6).Infof("Request was not an upgrade")
		return false
	}

	var (
		backendConn net.Conn
		rawResponse []byte
		err         error
	)

	location := *h.Location
	if h.UseRequestLocation {
		location = *req.URL
		location.Scheme = h.Location.Scheme
		location.Host = h.Location.Host
	}

	clone := utilnet.CloneRequest(req)
	// Only append X-Forwarded-For in the upgrade path, since httputil.NewSingleHostReverseProxy
	// handles this in the non-upgrade path.
	utilnet.AppendForwardedForHeader(clone)
	if h.InterceptRedirects {
		klog.V(6).Infof("Connecting to backend proxy (intercepting redirects) %s\n  Headers: %v", &location, clone.Header)
		backendConn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, &location, clone.Header, req.Body, utilnet.DialerFunc(h.DialForUpgrade), h.RequireSameHostRedirects)
	} else {
		klog.V(6).Infof("Connecting to backend proxy (direct dial) %s\n  Headers: %v", &location, clone.Header)
		clone.URL = &location
		backendConn, err = h.DialForUpgrade(clone)
	}
	if err != nil {
		klog.V(6).Infof("Proxy connection error: %v", err)
		h.Responder.Error(w, req, err)
		return true
	}
	defer backendConn.Close()

	// determine the http response code from the backend by reading from rawResponse+backendConn
	backendHTTPResponse, headerBytes, err := getResponse(io.MultiReader(bytes.NewReader(rawResponse), backendConn))
	if err != nil {
		klog.V(6).Infof("Proxy connection error: %v", err)
		h.Responder.Error(w, req, err)
		return true
	}
	if len(headerBytes) > len(rawResponse) {
		// we read beyond the bytes stored in rawResponse, update rawResponse to the full set of bytes read from the backend
		rawResponse = headerBytes
	}

	// If the backend did not upgrade the request, return an error to the client. If the response was
	// an error, the error is forwarded directly after the connection is hijacked. Otherwise, just
	// return a generic error here.
	if backendHTTPResponse.StatusCode != http.StatusSwitchingProtocols && backendHTTPResponse.StatusCode < 400 {
		err := fmt.Errorf("invalid upgrade response: status code %d", backendHTTPResponse.StatusCode)
		klog.Errorf("Proxy upgrade error: %v", err)
		h.Responder.Error(w, req, err)
		return true
	}

	// Once the connection is hijacked, the ErrorResponder will no longer work, so
	// hijacking should be the last step in the upgrade.
	requestHijacker, ok := w.(http.Hijacker)
	if !ok {
		klog.V(6).Infof("Unable to hijack response writer: %T", w)
		h.Responder.Error(w, req, fmt.Errorf("request connection cannot be hijacked: %T", w))
		return true
	}
	requestHijackedConn, _, err := requestHijacker.Hijack()
	if err != nil {
		klog.V(6).Infof("Unable to hijack response: %v", err)
		h.Responder.Error(w, req, fmt.Errorf("error hijacking connection: %v", err))
		return true
	}
	defer requestHijackedConn.Close()

	if backendHTTPResponse.StatusCode != http.StatusSwitchingProtocols {
		// If the backend did not upgrade the request, echo the response from the backend to the client and return, closing the connection.
		klog.V(6).Infof("Proxy upgrade error, status code %d", backendHTTPResponse.StatusCode)
		// set read/write deadlines
		deadline := time.Now().Add(10 * time.Second)
		backendConn.SetReadDeadline(deadline)
		requestHijackedConn.SetWriteDeadline(deadline)
		// write the response to the client
		err := backendHTTPResponse.Write(requestHijackedConn)
		if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
			klog.Errorf("Error proxying data from backend to client: %v", err)
		}
		// Indicate we handled the request
		return true
	}

	// Forward raw response bytes back to client.
	if len(rawResponse) > 0 {
		klog.V(6).Infof("Writing %d bytes to hijacked connection", len(rawResponse))
		if _, err = requestHijackedConn.Write(rawResponse); err != nil {
			utilruntime.HandleError(fmt.Errorf("Error proxying response from backend to client: %v", err))
		}
	}

	// Proxy the connection. This is bidirectional, so we need a goroutine
	// to copy in each direction. Once one side of the connection exits, we
	// exit the function which performs cleanup and in the process closes
	// the other half of the connection in the defer.
	writerComplete := make(chan struct{})
	readerComplete := make(chan struct{})

	go func() {
		var writer io.WriteCloser
		if h.MaxBytesPerSec > 0 {
			writer = flowrate.NewWriter(backendConn, h.MaxBytesPerSec)
		} else {
			writer = backendConn
		}
		_, err := io.Copy(writer, requestHijackedConn)
		if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
			klog.Errorf("Error proxying data from client to backend: %v", err)
		}
		close(writerComplete)
	}()

	go func() {
		var reader io.ReadCloser
		if h.MaxBytesPerSec > 0 {
			reader = flowrate.NewReader(backendConn, h.MaxBytesPerSec)
		} else {
			reader = backendConn
		}
		_, err := io.Copy(requestHijackedConn, reader)
		if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
			klog.Errorf("Error proxying data from backend to client: %v", err)
		}
		close(readerComplete)
	}()

	// Wait for one half the connection to exit. Once it does the defer will
	// clean up the other half of the connection.
	select {
	case <-writerComplete:
	case <-readerComplete:
	}
	klog.V(6).Infof("Disconnecting from backend proxy %s\n  Headers: %v", &location, clone.Header)

	return true
}

--- a/staging/src/k8s.io/apimachinery/pkg/util/proxy/upgradeaware.go
+++ b/staging/src/k8s.io/apimachinery/pkg/util/proxy/upgradeaware.go
@@ -46,6 +46,16 @@
 	if len(headerBytes) > len(rawResponse) {
 		// we read beyond the bytes stored in rawResponse, update rawResponse to the full set of bytes read from the backend
 		rawResponse = headerBytes
+	}
+
+	// If the backend did not upgrade the request, return an error to the client. If the response was
+	// an error, the error is forwarded directly after the connection is hijacked. Otherwise, just
+	// return a generic error here.
+	if backendHTTPResponse.StatusCode != http.StatusSwitchingProtocols && backendHTTPResponse.StatusCode < 400 {
+		err := fmt.Errorf("invalid upgrade response: status code %d", backendHTTPResponse.StatusCode)
+		klog.Errorf("Proxy upgrade error: %v", err)
+		h.Responder.Error(w, req, err)
+		return true
 	}

 	// Once the connection is hijacked, the ErrorResponder will no longer work, so

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

cd /workspace/kubernetes
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
go test  -timeout 30s -run ^TestProxyUpgradeErrorResponse$ k8s.io/apimachinery/pkg/util/proxy

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

cd /workspace/kubernetes
git apply --whitespace=nowarn /workspace/fix.patch
go test -timeout 30s -run '^(TestNewWarningHeader|TestGetClientIP|TestAppendForwardedForHeader|TestProxyUpgrade|TestTLSClientConfigHolder|TestConnectWithRedirects|TestP
roxyUpgradeErrorResponseTerminates|TestJoinPreservingTrailingSlash|TestE
rrorPropagation|TestSourceIPs|TestServeHTTP|TestProxyUpgradeErrorRespons
e|TestIsProbableEOF|TestFlushIntervalHeaders|TestParseWarningHeaders|Tes
tAllowsHTTP2|TestProxierWithNoProxyCIDR|TestParseWarningHeader|TestDefau
ltProxyTransport)$' k8s.io/apimachinery/pkg/util/proxy


A vulnerability, which was classified as problematic, has been found in UBI Reader up to 0.8.0. Affected by this issue is the function ubireader_extract_files of the file ubireader/ubifs/output.py of the component UBIFS File Handler. The manipulation leads to path traversal. The attack may be launched remotely. Upgrading to version 0.8.5 is able to address this issue. The name of the patch is d5d68e6b1b9f7070c29df5f67fc060f579ae9139. It is recommended to upgrade the affected component. VDB-216146 is the identifier assigned to this vulnerability.

def extract_dents(ubifs, inodes, dent_node, path='', perms=False):
    if dent_node.inum not in inodes:
        error(extract_dents, 'Error', 'inum: %s not found in inodes' % (dent_node.inum))
        return

    inode = inodes[dent_node.inum]
    dent_path = os.path.join(path, dent_node.name)

    if dent_node.type == UBIFS_ITYPE_DIR:
        try:
            if not os.path.exists(dent_path):
                os.mkdir(dent_path)
                log(extract_dents, 'Make Dir: %s' % (dent_path))

                if perms:
                    _set_file_perms(dent_path, inode)
        except Exception as e:
            error(extract_dents, 'Warn', 'DIR Fail: %s' % e)

        if 'dent' in inode:
            for dnode in inode['dent']:
                extract_dents(ubifs, inodes, dnode, dent_path, perms)

        _set_file_timestamps(dent_path, inode)

    elif dent_node.type == UBIFS_ITYPE_REG:
        try:
            if inode['ino'].nlink > 1:
                if 'hlink' not in inode:
                    inode['hlink'] = dent_path
                    buf = _process_reg_file(ubifs, inode, dent_path)
                    _write_reg_file(dent_path, buf)
                else:
                    os.link(inode['hlink'], dent_path)
                    log(extract_dents, 'Make Link: %s > %s' % (dent_path, inode['hlink']))
            else:
                buf = _process_reg_file(ubifs, inode, dent_path)
                _write_reg_file(dent_path, buf)

            _set_file_timestamps(dent_path, inode)

            if perms:
                _set_file_perms(dent_path, inode)

        except Exception as e:
            error(extract_dents, 'Warn', 'FILE Fail: %s' % e)

    elif dent_node.type == UBIFS_ITYPE_LNK:
        try:
            # probably will need to decompress ino data if > UBIFS_MIN_COMPR_LEN
            os.symlink('%s' % inode['ino'].data.decode('utf-8'), dent_path)
            log(extract_dents, 'Make Symlink: %s > %s' % (dent_path, inode['ino'].data))

        except Exception as e:
            error(extract_dents, 'Warn', 'SYMLINK Fail: %s' % e)

    elif dent_node.type in [UBIFS_ITYPE_BLK, UBIFS_ITYPE_CHR]:
        try:
            dev = struct.unpack('<II', inode['ino'].data)[0]
            if not settings.use_dummy_devices:
                os.mknod(dent_path, inode['ino'].mode, dev)
                log(extract_dents, 'Make Device Node: %s' % (dent_path))

                if perms:
                    _set_file_perms(dent_path, inode)
            else:
                log(extract_dents, 'Create dummy device.')
                _write_reg_file(dent_path, str(dev))

                if perms:
                    _set_file_perms(dent_path, inode)

        except Exception as e:
            error(extract_dents, 'Warn', 'DEV Fail: %s' % e)

    elif dent_node.type == UBIFS_ITYPE_FIFO:
        try:
            os.mkfifo(dent_path, inode['ino'].mode)
            log(extract_dents, 'Make FIFO: %s' % (path))

            if perms:
                _set_file_perms(dent_path, inode)
        except Exception as e:
            error(extract_dents, 'Warn', 'FIFO Fail: %s : %s' % (dent_path, e))

    elif dent_node.type == UBIFS_ITYPE_SOCK:
        try:
            if settings.use_dummy_socket_file:
                _write_reg_file(dent_path, '')
                if perms:
                    _set_file_perms(dent_path, inode)
        except Exception as e:
            error(extract_dents, 'Warn', 'SOCK Fail: %s : %s' % (dent_path, e))

def extract_dents(ubifs, inodes, dent_node, path='', perms=False):
    if dent_node.inum not in inodes:
        error(extract_dents, 'Error', 'inum: %s not found in inodes' % (dent_node.inum))
        return

    inode = inodes[dent_node.inum]

    if not is_safe_path(path, dent_node.name):
        error(extract_dents, 'Warning', 'Path traversal attempt: %s, discarding' % (dent_node.name))
        return
    dent_path = os.path.realpath(os.path.join(path, dent_node.name))

    if dent_node.type == UBIFS_ITYPE_DIR:
        try:
            if not os.path.exists(dent_path):
                os.mkdir(dent_path)
                log(extract_dents, 'Make Dir: %s' % (dent_path))

                if perms:
                    _set_file_perms(dent_path, inode)
        except Exception as e:
            error(extract_dents, 'Warn', 'DIR Fail: %s' % e)

        if 'dent' in inode:
            for dnode in inode['dent']:
                extract_dents(ubifs, inodes, dnode, dent_path, perms)

        _set_file_timestamps(dent_path, inode)

    elif dent_node.type == UBIFS_ITYPE_REG:
        try:
            if inode['ino'].nlink > 1:
                if 'hlink' not in inode:
                    inode['hlink'] = dent_path
                    buf = _process_reg_file(ubifs, inode, dent_path)
                    _write_reg_file(dent_path, buf)
                else:
                    os.link(inode['hlink'], dent_path)
                    log(extract_dents, 'Make Link: %s > %s' % (dent_path, inode['hlink']))
            else:
                buf = _process_reg_file(ubifs, inode, dent_path)
                _write_reg_file(dent_path, buf)

            _set_file_timestamps(dent_path, inode)

            if perms:
                _set_file_perms(dent_path, inode)

        except Exception as e:
            error(extract_dents, 'Warn', 'FILE Fail: %s' % e)

    elif dent_node.type == UBIFS_ITYPE_LNK:
        try:
            # probably will need to decompress ino data if > UBIFS_MIN_COMPR_LEN
            os.symlink('%s' % inode['ino'].data.decode('utf-8'), dent_path)
            log(extract_dents, 'Make Symlink: %s > %s' % (dent_path, inode['ino'].data))

        except Exception as e:
            error(extract_dents, 'Warn', 'SYMLINK Fail: %s' % e)

    elif dent_node.type in [UBIFS_ITYPE_BLK, UBIFS_ITYPE_CHR]:
        try:
            dev = struct.unpack('<II', inode['ino'].data)[0]
            if not settings.use_dummy_devices:
                os.mknod(dent_path, inode['ino'].mode, dev)
                log(extract_dents, 'Make Device Node: %s' % (dent_path))

                if perms:
                    _set_file_perms(dent_path, inode)
            else:
                log(extract_dents, 'Create dummy device.')
                _write_reg_file(dent_path, str(dev))

                if perms:
                    _set_file_perms(dent_path, inode)

        except Exception as e:
            error(extract_dents, 'Warn', 'DEV Fail: %s' % e)

    elif dent_node.type == UBIFS_ITYPE_FIFO:
        try:
            os.mkfifo(dent_path, inode['ino'].mode)
            log(extract_dents, 'Make FIFO: %s' % (path))

            if perms:
                _set_file_perms(dent_path, inode)
        except Exception as e:
            error(extract_dents, 'Warn', 'FIFO Fail: %s : %s' % (dent_path, e))

    elif dent_node.type == UBIFS_ITYPE_SOCK:
        try:
            if settings.use_dummy_socket_file:
                _write_reg_file(dent_path, '')
                if perms:
                    _set_file_perms(dent_path, inode)
        except Exception as e:
            error(extract_dents, 'Warn', 'SOCK Fail: %s : %s' % (dent_path, e))

--- a/ubireader/ubifs/output.py
+++ b/ubireader/ubifs/output.py
@@ -4,8 +4,12 @@
         return

     inode = inodes[dent_node.inum]
-    dent_path = os.path.join(path, dent_node.name)
-
+
+    if not is_safe_path(path, dent_node.name):
+        error(extract_dents, 'Warning', 'Path traversal attempt: %s, discarding' % (dent_node.name))
+        return
+    dent_path = os.path.realpath(os.path.join(path, dent_node.name))
+
     if dent_node.type == UBIFS_ITYPE_DIR:
         try:
             if not os.path.exists(dent_path):


HedgeDoc is a collaborative platform for writing and sharing markdown. In HedgeDoc before version 1.7.1 an unauthenticated attacker can upload arbitrary files to the upload storage backend including HTML, JS and PHP files. The problem is patched in HedgeDoc 1.7.1. You should however verify that your uploaded file storage only contains files that are allowed, as uploaded files might still be served. As workaround it's possible to block the `/uploadimage` endpoint on your instance using your reverse proxy. And/or restrict MIME-types and file names served from your upload file storage.

Unrestricted Upload of File with Dangerous Type

The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.

exports.uploadImage = function (imagePath, callback) {
  if (!callback || typeof callback !== 'function') {
    logger.error('Callback has to be a function')
    return
  }

  if (!imagePath || typeof imagePath !== 'string') {
    callback(new Error('Image path is missing or wrong'), null)
    return
  }

  callback(null, (new URL(path.basename(imagePath), config.serverURL + '/uploads/')).href)
}

imageRouter.post('/uploadimage', function (req, res) {
  var form = new formidable.IncomingForm()

  form.keepExtensions = true

  if (config.imageUploadType === 'filesystem') {
    form.uploadDir = config.uploadsPath
  }

  form.parse(req, function (err, fields, files) {
    if (err || !files.image || !files.image.path) {
      logger.error(`formidable error: ${err}`)
      errors.errorForbidden(res)
    } else {
      logger.debug(`SERVER received uploadimage: ${JSON.stringify(files.image)}`)

      const uploadProvider = require('./' + config.imageUploadType)
      logger.debug(`imageRouter: Uploading ${files.image.path} using ${config.imageUploadType}`)
      uploadProvider.uploadImage(files.image.path, function (err, url) {
        if (err !== null) {
          logger.error(err)
          return res.status(500).end('upload image error')
        }
        logger.debug(`SERVER sending ${url} to client`)
        res.send({
          link: url
        })
      })
    }
  })
})

exports.uploadImage = function (imagePath, callback) {
  if (!callback || typeof callback !== 'function') {
    logger.error('Callback has to be a function')
    return
  }

  if (!imagePath || typeof imagePath !== 'string') {
    callback(new Error('Image path is missing or wrong'), null)
    return
  }

  const fileName = path.basename(imagePath)
  // move image from temporary path to upload directory
  try {
    fs.copyFileSync(imagePath, path.join(config.uploadsPath, fileName))
  } catch (e) {
    callback(new Error('Error while moving file'), null)
    return
  }
  callback(null, (new URL(fileName, config.serverURL + '/uploads/')).href)
}

async function checkUploadType (filePath) {
  const typeFromMagic = await FileType.fromFile(filePath)
  if (typeFromMagic === undefined) {
    logger.error(`Image upload error: Could not determine MIME-type`)
    return false
  }
  if (path.extname(filePath) !== '.' + typeFromMagic.ext) {
    logger.error(`Image upload error: Provided file extension does not match MIME-type`)
    return false
  }
  if (!config.allowedUploadMimeTypes.includes(typeFromMagic.mime)) {
    logger.error(`Image upload error: MIME-type "${typeFromMagic.mime}" of uploaded file not allowed, only "${config.allowedUploadMimeTypes.join(', ')}" are allowed`)
    return false
  }
  return true
}

imageRouter.post('/uploadimage', function (req, res) {
  if (!req.isAuthenticated() && !config.allowAnonymous && !config.allowAnonymousEdits) {
    logger.error(`Image upload error: Anonymous edits and therefore uploads are not allowed)`)
    return errors.errorForbidden(res)
  }

  var form = new formidable.IncomingForm()
  form.keepExtensions = true
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hedgedoc-'))
  form.uploadDir = tmpDir

  form.parse(req, async function (err, fields, files) {
    if (err) {
      logger.error(`Image upload error: formidable error: ${err}`)
      rimraf(tmpDir)
      return errors.errorForbidden(res)
    } else if (!files.image || !files.image.path) {
      logger.error(`Image upload error: Upload didn't contain file)`)
      rimraf.sync(tmpDir)
      return errors.errorBadRequest(res)
    } else if (!await checkUploadType(files.image.path)) {
      rimraf.sync(tmpDir)
      return errors.errorBadRequest(res)
    } else {
      logger.debug(`SERVER received uploadimage: ${JSON.stringify(files.image)}`)

      const uploadProvider = require('./' + config.imageUploadType)
      logger.debug(`imageRouter: Uploading ${files.image.path} using ${config.imageUploadType}`)
      uploadProvider.uploadImage(files.image.path, function (err, url) {
        rimraf.sync(tmpDir)
        if (err !== null) {
          logger.error(err)
          return res.status(500).end('upload image error')
        }
        logger.debug(`SERVER sending ${url} to client`)
        res.send({
          link: url
        })
      })
    }
  })
})

--- a/lib/web/imageRouter/filesystem.js
+++ b/lib/web/imageRouter/filesystem.js
@@ -9,5 +9,13 @@
     return
   }

-  callback(null, (new URL(path.basename(imagePath), config.serverURL + '/uploads/')).href)
+  const fileName = path.basename(imagePath)
+  // move image from temporary path to upload directory
+  try {
+    fs.copyFileSync(imagePath, path.join(config.uploadsPath, fileName))
+  } catch (e) {
+    callback(new Error('Error while moving file'), null)
+    return
+  }
+  callback(null, (new URL(fileName, config.serverURL + '/uploads/')).href)
 }

--- a/lib/web/imageRouter/index.js
+++ b/lib/web/imageRouter/index.js
@@ -1,31 +1,16 @@
-imageRouter.post('/uploadimage', function (req, res) {
-  var form = new formidable.IncomingForm()
-
-  form.keepExtensions = true
-
-  if (config.imageUploadType === 'filesystem') {
-    form.uploadDir = config.uploadsPath
+async function checkUploadType (filePath) {
+  const typeFromMagic = await FileType.fromFile(filePath)
+  if (typeFromMagic === undefined) {
+    logger.error(`Image upload error: Could not determine MIME-type`)
+    return false
   }
-
-  form.parse(req, function (err, fields, files) {
-    if (err || !files.image || !files.image.path) {
-      logger.error(`formidable error: ${err}`)
-      errors.errorForbidden(res)
-    } else {
-      logger.debug(`SERVER received uploadimage: ${JSON.stringify(files.image)}`)
-
-      const uploadProvider = require('./' + config.imageUploadType)
-      logger.debug(`imageRouter: Uploading ${files.image.path} using ${config.imageUploadType}`)
-      uploadProvider.uploadImage(files.image.path, function (err, url) {
-        if (err !== null) {
-          logger.error(err)
-          return res.status(500).end('upload image error')
-        }
-        logger.debug(`SERVER sending ${url} to client`)
-        res.send({
-          link: url
-        })
-      })
-    }
-  })
-})
+  if (path.extname(filePath) !== '.' + typeFromMagic.ext) {
+    logger.error(`Image upload error: Provided file extension does not match MIME-type`)
+    return false
+  }
+  if (!config.allowedUploadMimeTypes.includes(typeFromMagic.mime)) {
+    logger.error(`Image upload error: MIME-type "${typeFromMagic.mime}" of uploaded file not allowed, only "${config.allowedUploadMimeTypes.join(', ')}" are allowed`)
+    return false
+  }
+  return true
+}

--- /dev/null
+++ b/lib/web/imageRouter/index.js
@@ -0,0 +1,42 @@
+imageRouter.post('/uploadimage', function (req, res) {
+  if (!req.isAuthenticated() && !config.allowAnonymous && !config.allowAnonymousEdits) {
+    logger.error(`Image upload error: Anonymous edits and therefore uploads are not allowed)`)
+    return errors.errorForbidden(res)
+  }
+
+  var form = new formidable.IncomingForm()
+  form.keepExtensions = true
+  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hedgedoc-'))
+  form.uploadDir = tmpDir
+
+  form.parse(req, async function (err, fields, files) {
+    if (err) {
+      logger.error(`Image upload error: formidable error: ${err}`)
+      rimraf(tmpDir)
+      return errors.errorForbidden(res)
+    } else if (!files.image || !files.image.path) {
+      logger.error(`Image upload error: Upload didn't contain file)`)
+      rimraf.sync(tmpDir)
+      return errors.errorBadRequest(res)
+    } else if (!await checkUploadType(files.image.path)) {
+      rimraf.sync(tmpDir)
+      return errors.errorBadRequest(res)
+    } else {
+      logger.debug(`SERVER received uploadimage: ${JSON.stringify(files.image)}`)
+
+      const uploadProvider = require('./' + config.imageUploadType)
+      logger.debug(`imageRouter: Uploading ${files.image.path} using ${config.imageUploadType}`)
+      uploadProvider.uploadImage(files.image.path, function (err, url) {
+        rimraf.sync(tmpDir)
+        if (err !== null) {
+          logger.error(err)
+          return res.status(500).end('upload image error')
+        }
+        logger.debug(`SERVER sending ${url} to client`)
+        res.send({
+          link: url
+        })
+      })
+    }
+  })
+})


tarteaucitron.js is a compliant and accessible cookie banner. A vulnerability was identified in tarteaucitron.js prior to 1.20.1, where the addOrUpdate function, used for applying custom texts, did not properly validate input. This allowed an attacker with direct access to the site's source code or a CMS plugin to manipulate JavaScript object prototypes, leading to potential security risks such as data corruption or unintended code execution. An attacker with high privileges could exploit this vulnerability to modify object prototypes, affecting core JavaScript behavior, cause application crashes or unexpected behavior, or potentially introduce further security vulnerabilities depending on the application's architecture. This vulnerability is fixed in 1.20.1.

Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')

The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.

    "AddOrUpdate" : function(source, custom){
        /**
         Utility function to Add or update the fields of obj1 with the ones in obj2
         */
        for(var key in custom){
            if(custom[key] instanceof Object){
                source[key] = tarteaucitron.AddOrUpdate(source[key], custom[key]);
            }else{
                source[key] = custom[key];
            }
        }
        return source;
    },

    "AddOrUpdate" : function(source, custom){
        /**
         Utility function to Add or update the fields of obj1 with the ones in obj2
         */
        for(var key in custom){
            if (key === "__proto__" || key === "constructor") continue;
            if(custom.hasOwnProperty(key)){
                if(custom[key] instanceof Object){
                    source[key] = tarteaucitron.AddOrUpdate(source[key], custom[key]);
                }else{
                    source[key] = custom[key];
                }
            }
        }
        return source;
    },

--- a/tarteaucitron.js
+++ b/tarteaucitron.js
@@ -3,10 +3,13 @@
          Utility function to Add or update the fields of obj1 with the ones in obj2
          */
         for(var key in custom){
-            if(custom[key] instanceof Object){
-                source[key] = tarteaucitron.AddOrUpdate(source[key], custom[key]);
-            }else{
-                source[key] = custom[key];
+            if (key === "__proto__" || key === "constructor") continue;
+            if(custom.hasOwnProperty(key)){
+                if(custom[key] instanceof Object){
+                    source[key] = tarteaucitron.AddOrUpdate(source[key], custom[key]);
+                }else{
+                    source[key] = custom[key];
+                }
             }
         }
         return source;


Pug through 3.0.2 allows JavaScript code execution if an application accepts untrusted input for the name option of the compileClient, compileFileClient, or compileClientWithDependenciesTracked function. NOTE: these functions are for compiling Pug templates into JavaScript, and there would typically be no reason to allow untrusted callers.

function Compiler(node, options) {
  this.options = options = options || {};
  this.node = node;
  this.bufferedConcatenationCount = 0;
  this.hasCompiledDoctype = false;
  this.hasCompiledTag = false;
  this.pp = options.pretty || false;
  if (this.pp && typeof this.pp !== 'string') {
    this.pp = '  ';
  }
  if (this.pp && !/^\s+$/.test(this.pp)) {
    throw new Error(
      'The pretty parameter should either be a boolean or whitespace only string'
    );
  }
  this.debug = false !== options.compileDebug;
  this.indents = 0;
  this.parentIndents = 0;
  this.terse = false;
  this.mixins = {};
  this.dynamicMixins = false;
  this.eachCount = 0;
  if (options.doctype) this.setDoctype(options.doctype);
  this.runtimeFunctionsUsed = [];
  this.inlineRuntimeFunctions = options.inlineRuntimeFunctions || false;
  if (this.debug && this.inlineRuntimeFunctions) {
    this.runtimeFunctionsUsed.push('rethrow');
  }
}

function Compiler(node, options) {
  this.options = options = options || {};
  this.node = node;
  this.bufferedConcatenationCount = 0;
  this.hasCompiledDoctype = false;
  this.hasCompiledTag = false;
  this.pp = options.pretty || false;
  if (this.pp && typeof this.pp !== 'string') {
    this.pp = '  ';
  }
  if (this.pp && !/^\s+$/.test(this.pp)) {
    throw new Error(
      'The pretty parameter should either be a boolean or whitespace only string'
    );
  }
  if (this.options.templateName && !isIdentifier(this.options.templateName)) {
    throw new Error(
      'The templateName parameter must be a valid JavaScript identifier if specified.'
    );
  }
  if (
    this.doctype &&
    (this.doctype.includes('<') || this.doctype.includes('>'))
  ) {
    throw new Error('Doctype can not contain "<" or ">"');
  }
  if (this.options.globals && !this.options.globals.every(isIdentifier)) {
    throw new Error(
      'The globals option must be an array of valid JavaScript identifiers if specified.'
    );
  }

  this.debug = false !== options.compileDebug;
  this.indents = 0;
  this.parentIndents = 0;
  this.terse = false;
  this.mixins = {};
  this.dynamicMixins = false;
  this.eachCount = 0;
  if (options.doctype) this.setDoctype(options.doctype);
  this.runtimeFunctionsUsed = [];
  this.inlineRuntimeFunctions = options.inlineRuntimeFunctions || false;
  if (this.debug && this.inlineRuntimeFunctions) {
    this.runtimeFunctionsUsed.push('rethrow');
  }
}

function isIdentifier(name) {
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
}

--- a/packages/pug-code-gen/index.js
+++ b/packages/pug-code-gen/index.js
@@ -13,6 +13,23 @@
       'The pretty parameter should either be a boolean or whitespace only string'
     );
   }
+  if (this.options.templateName && !isIdentifier(this.options.templateName)) {
+    throw new Error(
+      'The templateName parameter must be a valid JavaScript identifier if specified.'
+    );
+  }
+  if (
+    this.doctype &&
+    (this.doctype.includes('<') || this.doctype.includes('>'))
+  ) {
+    throw new Error('Doctype can not contain "<" or ">"');
+  }
+  if (this.options.globals && !this.options.globals.every(isIdentifier)) {
+    throw new Error(
+      'The globals option must be an array of valid JavaScript identifiers if specified.'
+    );
+  }
+
   this.debug = false !== options.compileDebug;
   this.indents = 0;
   this.parentIndents = 0;

--- /dev/null
+++ b/packages/pug-code-gen/index.js
@@ -0,0 +1,3 @@
+function isIdentifier(name) {
+  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
+}


Kiwi TCMS is an open source test management system for both manual and automated testing. Kiwi TCMS allows users to upload attachments to test plans, test cases, etc. Earlier versions of Kiwi TCMS had introduced upload validators in order to prevent potentially dangerous files from being uploaded and Content-Security-Policy definition to prevent cross-site-scripting attacks. The upload validation checks were not 100% robust which left the possibility to circumvent them and upload a potentially dangerous file which allows execution of arbitrary JavaScript in the browser. Additionally we've discovered that Nginx's `proxy_pass` directive will strip some headers negating protections built into Kiwi TCMS when served behind a reverse proxy. This issue has been addressed in version 12.4. Users are advised to upgrade. Users unable to upgrade who are serving Kiwi TCMS behind a reverse proxy should make sure that additional header values are still passed to the client browser. If they aren't redefining them inside the proxy configuration.

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.

def deny_uploads_containing_script_tag(uploaded_file):
    for chunk in uploaded_file.chunks(2048):
        if chunk.lower().find(b"<script") > -1:
            raise ValidationError(_("File contains forbidden <script> tag"))

        if chunk.lower().find(b"onload=") > -1:
            raise ValidationError(_("File contains forbidden attribute:") + "onload")

--- a/tcms/kiwi_attachments/validators.py
+++ b/tcms/kiwi_attachments/validators.py
@@ -2,3 +2,6 @@
     for chunk in uploaded_file.chunks(2048):
         if chunk.lower().find(b"<script") > -1:
             raise ValidationError(_("File contains forbidden <script> tag"))
+
+        if chunk.lower().find(b"onload=") > -1:
+            raise ValidationError(_("File contains forbidden attribute:") + "onload")

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

cd /workspace/kiwi
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
/workspace/PoC_env/CVE-2023-33977/bin/python  hand_test.py