The Snowflake Connector for Python provides an interface for developing Python applications that can connect to Snowflake and perform all standard operations. Versions prior to 3.0.2 are vulnerable to command injection via single sign-on(SSO) browser URL authentication. In order to exploit the potential for command injection, an attacker would need to be successful in (1) establishing a malicious resource and (2) redirecting users to utilize the resource. The attacker could set up a malicious, publicly accessible server which responds to the SSO URL with an attack payload. If the attacker then tricked a user into visiting the maliciously crafted connection URL, the user’s local machine would render the malicious payload, leading to a remote code execution. This attack scenario can be mitigated through URL whitelisting as well as common anti-phishing resources. Version 3.0.2 contains a patch for 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 warn(  # type: ignore[override]
        self,
        msg: str,
        path_name: str | None = None,
        func_name: str | None = None,
        *args: Any,
        **kwargs: Any,
    ) -> None:
        warnings.warn(
            "The 'warn' method is deprecated, " "use 'warning' instead",
            DeprecationWarning,
            2,
        )
        self.warning(msg, path_name, func_name, *args, **kwargs)

    def prepare(
        self,
        *,
        conn: SnowflakeConnection,
        authenticator: str,
        service_name: str | None,
        account: str,
        user: str,
        **kwargs: Any,
    ) -> None:
        """Web Browser based Authentication."""
        logger.debug("authenticating by Web Browser")

        socket_connection = self._socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            try:
                socket_connection.bind(
                    (
                        os.getenv("SF_AUTH_SOCKET_ADDR", "localhost"),
                        int(os.getenv("SF_AUTH_SOCKET_PORT", 0)),
                    )
                )
            except socket.gaierror as ex:
                if ex.args[0] == socket.EAI_NONAME:
                    raise OperationalError(
                        msg="localhost is not found. Ensure /etc/hosts has "
                        "localhost entry.",
                        errno=ER_NO_HOSTNAME_FOUND,
                    )
                else:
                    raise ex
            socket_connection.listen(0)  # no backlog
            callback_port = socket_connection.getsockname()[1]

            print(
                "Initiating login request with your identity provider. A "
                "browser window should have opened for you to complete the "
                "login. If you can't see it, check existing browser windows, "
                "or your OS settings. Press CTRL+C to abort and try again..."
            )

            logger.debug("step 1: query GS to obtain SSO url")
            sso_url = self._get_sso_url(
                conn, authenticator, service_name, account, callback_port, user
            )

            logger.debug("step 2: open a browser")
            print(f"Going to open: {sso_url} to authenticate...")
            if not self._webbrowser.open_new(sso_url):
                print(
                    "We were unable to open a browser window for you, "
                    "please open the url above manually then paste the "
                    "URL you are redirected to into the terminal."
                )
                url = input("Enter the URL the SSO URL redirected you to: ")
                self._process_get_url(url)
                if not self._token:
                    # Input contained no token, either URL was incorrectly pasted,
                    # empty or just wrong
                    self._handle_failure(
                        conn=conn,
                        ret={
                            "code": ER_UNABLE_TO_OPEN_BROWSER,
                            "message": (
                                "Unable to open a browser in this environment and "
                                "SSO URL contained no token"
                            ),
                        },
                    )
                    return
            else:
                logger.debug("step 3: accept SAML token")
                self._receive_saml_token(conn, socket_connection)
        finally:
            socket_connection.close()

    def warn(  # type: ignore[override]
        self,
        msg: str,
        path_name: str | None = None,
        func_name: str | None = None,
        *args: Any,
        **kwargs: Any,
    ) -> None:
        warnings.warn(
            "The 'warn' method is deprecated, " "use 'warning' instead",
            DeprecationWarning,
            stacklevel=2,
        )
        self.warning(msg, path_name, func_name, *args, **kwargs)

    def prepare(
        self,
        *,
        conn: SnowflakeConnection,
        authenticator: str,
        service_name: str | None,
        account: str,
        user: str,
        **kwargs: Any,
    ) -> None:
        """Web Browser based Authentication."""
        logger.debug("authenticating by Web Browser")

        socket_connection = self._socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            try:
                socket_connection.bind(
                    (
                        os.getenv("SF_AUTH_SOCKET_ADDR", "localhost"),
                        int(os.getenv("SF_AUTH_SOCKET_PORT", 0)),
                    )
                )
            except socket.gaierror as ex:
                if ex.args[0] == socket.EAI_NONAME:
                    raise OperationalError(
                        msg="localhost is not found. Ensure /etc/hosts has "
                        "localhost entry.",
                        errno=ER_NO_HOSTNAME_FOUND,
                    )
                else:
                    raise ex
            socket_connection.listen(0)  # no backlog
            callback_port = socket_connection.getsockname()[1]

            logger.debug("step 1: query GS to obtain SSO url")
            sso_url = self._get_sso_url(
                conn, authenticator, service_name, account, callback_port, user
            )

            logger.debug("Validate SSO URL")
            if not is_valid_url(sso_url):
                self._handle_failure(
                    conn=conn,
                    ret={
                        "code": ER_INVALID_VALUE,
                        "message": (f"The SSO URL provided {sso_url} is invalid"),
                    },
                )
                return

            print(
                "Initiating login request with your identity provider. A "
                "browser window should have opened for you to complete the "
                "login. If you can't see it, check existing browser windows, "
                "or your OS settings. Press CTRL+C to abort and try again..."
            )

            logger.debug("step 2: open a browser")
            print(f"Going to open: {sso_url} to authenticate...")
            if not self._webbrowser.open_new(sso_url):
                print(
                    "We were unable to open a browser window for you, "
                    "please open the url above manually then paste the "
                    "URL you are redirected to into the terminal."
                )
                url = input("Enter the URL the SSO URL redirected you to: ")
                self._process_get_url(url)
                if not self._token:
                    # Input contained no token, either URL was incorrectly pasted,
                    # empty or just wrong
                    self._handle_failure(
                        conn=conn,
                        ret={
                            "code": ER_UNABLE_TO_OPEN_BROWSER,
                            "message": (
                                "Unable to open a browser in this environment and "
                                "SSO URL contained no token"
                            ),
                        },
                    )
                    return
            else:
                logger.debug("step 3: accept SAML token")
                self._receive_saml_token(conn, socket_connection)
        finally:
            socket_connection.close()

--- a/src/snowflake/connector/snow_logging.py
+++ b/src/snowflake/connector/snow_logging.py
@@ -9,6 +9,6 @@
         warnings.warn(
             "The 'warn' method is deprecated, " "use 'warning' instead",
             DeprecationWarning,
-            2,
+            stacklevel=2,
         )
         self.warning(msg, path_name, func_name, *args, **kwargs)

--- a/src/snowflake/connector/auth/webbrowser.py
+++ b/src/snowflake/connector/auth/webbrowser.py
@@ -32,16 +32,27 @@
             socket_connection.listen(0)  # no backlog
             callback_port = socket_connection.getsockname()[1]

+            logger.debug("step 1: query GS to obtain SSO url")
+            sso_url = self._get_sso_url(
+                conn, authenticator, service_name, account, callback_port, user
+            )
+
+            logger.debug("Validate SSO URL")
+            if not is_valid_url(sso_url):
+                self._handle_failure(
+                    conn=conn,
+                    ret={
+                        "code": ER_INVALID_VALUE,
+                        "message": (f"The SSO URL provided {sso_url} is invalid"),
+                    },
+                )
+                return
+
             print(
                 "Initiating login request with your identity provider. A "
                 "browser window should have opened for you to complete the "
                 "login. If you can't see it, check existing browser windows, "
                 "or your OS settings. Press CTRL+C to abort and try again..."
-            )
-
-            logger.debug("step 1: query GS to obtain SSO url")
-            sso_url = self._get_sso_url(
-                conn, authenticator, service_name, account, callback_port, user
             )

             logger.debug("step 2: open a browser")

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

cd /workspace/snowflake-connector-python
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
/workspace/PoC_env/CVE-2023-34233/bin/python -m pytest test/unit/test_url_util.py test/unit/test_auth_webbrowser.py -v -k "test_auth_webbrowser_invalid_sso" && /workspace/PoC_env/CVE-2023-34233/bin/python hand_test.py

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

cd /workspace/snowflake-connector-python
git apply --whitespace=nowarn /workspace/fix.patch
/workspace/PoC_env/CVE-2023-34233/bin/python -m pytest test/unit/test_auth_webbrowser.py -v


`effectindex/tripreporter` is a community-powered, universal platform for submitting and analyzing trip reports. Prior to commit bd80ba833b9023d39ca22e29874296c8729dd53b, any user with an account on an instance of `effectindex/tripreporter`, e.g. `subjective.report`, may be affected by an improper password verification vulnerability. The vulnerability allows any user with a password matching the password requirements to log in as any user. This allows access to accounts / data loss of the user. This issue is patched in commit bd80ba833b9023d39ca22e29874296c8729dd53b. No action necessary for users of `subjective.report`, and anyone running their own instance should update to this commit or newer as soon as possible. As a workaround, someone running their own instance may apply the patch manually.

Improper Authentication

When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.

func AccountPostLogin(w http.ResponseWriter, r *http.Request) {
	account, err := (&models.Account{Context: ctx.Context}).FromBody(r)
	if err != nil {
		ctx.HandleStatus(w, r, err.Error(), http.StatusBadRequest)
		return
	}

	var a1 = &models.Account{Context: ctx.Context}
	a1.FromData(account)
	a1, err = a1.Get()
	if err != nil {
		ctx.HandleStatus(w, r, err.Error(), http.StatusBadRequest)
		return
	}

	account, err = a1.ValidatePassword(account.Password, "Password")
	if err != nil {
		ctx.HandleStatus(w, r, "Invalid username or password!", http.StatusForbidden)
		return
	}

	session, err := (&models.Session{Context: ctx.Context, Unique: account.Unique}).Post()
	if err != nil {
		ctx.HandleStatus(w, r, err.Error(), http.StatusBadRequest)
		return
	}

	expiry := time.Now().Add(time.Hour * 15) // TODO: Change this once we've implemented refreshing
	SetAuthCookie(w, types.CookieSessionID, session.Key.ID.String(), expiry)
	SetAuthCookie(w, types.CookieRefreshToken, session.Refresh, expiry)

	ctx.HandleJson(w, r, account.CopyPublic(), http.StatusOK)
}

func AccountPostLogin(w http.ResponseWriter, r *http.Request) {
	account, err := (&models.Account{Context: ctx.Context}).FromBody(r)
	if err != nil {
		ctx.HandleStatus(w, r, err.Error(), http.StatusBadRequest)
		return
	}

	var a1 = &models.Account{Context: ctx.Context}
	a1.FromData(account)
	a1, err = a1.Get()
	if err != nil {
		ctx.HandleStatus(w, r, err.Error(), http.StatusBadRequest)
		return
	}

	account, err = a1.VerifyPassword(account.Password)
	if err != nil {
		ctx.HandleStatus(w, r, "Invalid username or password!", http.StatusForbidden)
		return
	}

	session, err := (&models.Session{Context: ctx.Context, Unique: account.Unique}).Post()
	if err != nil {
		ctx.HandleStatus(w, r, err.Error(), http.StatusBadRequest)
		return
	}

	expiry := time.Now().Add(time.Hour * 15) // TODO: Change this once we've implemented refreshing
	SetAuthCookie(w, types.CookieSessionID, session.Key.ID.String(), expiry)
	SetAuthCookie(w, types.CookieRefreshToken, session.Refresh, expiry)

	ctx.HandleJson(w, r, account.CopyPublic(), http.StatusOK)
}

--- a/api/account.go
+++ b/api/account.go
@@ -13,7 +13,7 @@
 		return
 	}

-	account, err = a1.ValidatePassword(account.Password, "Password")
+	account, err = a1.VerifyPassword(account.Password)
 	if err != nil {
 		ctx.HandleStatus(w, r, "Invalid username or password!", http.StatusForbidden)
 		return


It was discovered that sos-collector does not properly set the default permissions of newly created files, making all files created by the tool readable by any local user. A local attacker may use this flaw by waiting for a legit user to run sos-collector and steal the collected data in the /var/tmp directory.

Incorrect Permission Assignment for Critical Resource

The product specifies permissions for a security-critical resource in a way that allows that resource to be read or modified by unintended actors.

    def __init__(self, config):
        self.config = config
        self.threads = []
        self.workers = []
        self.client_list = []
        self.node_list = []
        self.master = False
        self.retrieved = 0
        self.need_local_sudo = False
        if not self.config['list_options']:
            try:
                if not self.config['tmp_dir']:
                    self.create_tmp_dir()
                self._setup_logging()
                self.log_debug('Executing %s' % ' '.join(s for s in sys.argv))
                self._load_clusters()
                self._parse_options()
                self.prep()
            except KeyboardInterrupt:
                self._exit('Exiting on user cancel', 130)
        else:
            self._load_clusters()

    def __init__(self, config):
        os.umask(0077)
        self.config = config
        self.threads = []
        self.workers = []
        self.client_list = []
        self.node_list = []
        self.master = False
        self.retrieved = 0
        self.need_local_sudo = False
        if not self.config['list_options']:
            try:
                if not self.config['tmp_dir']:
                    self.create_tmp_dir()
                self._setup_logging()
                self.log_debug('Executing %s' % ' '.join(s for s in sys.argv))
                self._load_clusters()
                self._parse_options()
                self.prep()
            except KeyboardInterrupt:
                self._exit('Exiting on user cancel', 130)
        else:
            self._load_clusters()

--- a/soscollector/sos_collector.py
+++ b/soscollector/sos_collector.py
@@ -1,4 +1,5 @@
     def __init__(self, config):
+        os.umask(0077)
         self.config = config
         self.threads = []
         self.workers = []


Zulip is an open-source team collaboration tool with unique topic-based threading. Zulip administrators can configure Zulip to limit who can add users to streams, and separately to limit who can invite users to the organization. In Zulip Server 6.1 and below, the UI which allows a user to invite a new user also allows them to set the streams that the new user is invited to -- even if the inviting user would not have permissions to add an existing user to streams. While such a configuration is likely rare in practice, the behavior does violate security-related controls. This does not let a user invite new users to streams they cannot see, or would not be able to add users to if they had that general permission. This issue has been addressed in version 6.2. Users are advised to upgrade. Users unable to upgrade may limit sending of invitations down to users who also have the permission to add users to streams.

Missing Authorization

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

def invite_users_backend(
    request: HttpRequest,
    user_profile: UserProfile,
    invitee_emails_raw: str = REQ("invitee_emails"),
    invite_expires_in_minutes: Optional[int] = REQ(
        json_validator=check_none_or(check_int), default=INVITATION_LINK_VALIDITY_MINUTES
    ),
    invite_as: int = REQ(
        json_validator=check_int_in(
            list(PreregistrationUser.INVITE_AS.values()),
        ),
        default=PreregistrationUser.INVITE_AS["MEMBER"],
    ),
    stream_ids: List[int] = REQ(json_validator=check_list(check_int)),
) -> HttpResponse:
    if not user_profile.can_invite_others_to_realm():
        # Guest users case will not be handled here as it will
        # be handled by the decorator above.
        raise JsonableError(_("Insufficient permission"))
    check_if_owner_required(invite_as, user_profile)
    if (
        invite_as
        in [
            PreregistrationUser.INVITE_AS["REALM_ADMIN"],
            PreregistrationUser.INVITE_AS["MODERATOR"],
        ]
        and not user_profile.is_realm_admin
    ):
        raise JsonableError(_("Must be an organization administrator"))
    if not invitee_emails_raw:
        raise JsonableError(_("You must specify at least one email address."))

    invitee_emails = get_invitee_emails_set(invitee_emails_raw)

    streams: List[Stream] = []
    for stream_id in stream_ids:
        try:
            (stream, sub) = access_stream_by_id(user_profile, stream_id)
        except JsonableError:
            raise JsonableError(
                _("Stream does not exist with id: {}. No invites were sent.").format(stream_id)
            )
        streams.append(stream)

    do_invite_users(
        user_profile,
        invitee_emails,
        streams,
        invite_expires_in_minutes=invite_expires_in_minutes,
        invite_as=invite_as,
    )
    return json_success(request)

def invite_users_backend(
    request: HttpRequest,
    user_profile: UserProfile,
    invitee_emails_raw: str = REQ("invitee_emails"),
    invite_expires_in_minutes: Optional[int] = REQ(
        json_validator=check_none_or(check_int), default=INVITATION_LINK_VALIDITY_MINUTES
    ),
    invite_as: int = REQ(
        json_validator=check_int_in(
            list(PreregistrationUser.INVITE_AS.values()),
        ),
        default=PreregistrationUser.INVITE_AS["MEMBER"],
    ),
    stream_ids: List[int] = REQ(json_validator=check_list(check_int)),
) -> HttpResponse:
    if not user_profile.can_invite_others_to_realm():
        # Guest users case will not be handled here as it will
        # be handled by the decorator above.
        raise JsonableError(_("Insufficient permission"))
    check_if_owner_required(invite_as, user_profile)
    if (
        invite_as
        in [
            PreregistrationUser.INVITE_AS["REALM_ADMIN"],
            PreregistrationUser.INVITE_AS["MODERATOR"],
        ]
        and not user_profile.is_realm_admin
    ):
        raise JsonableError(_("Must be an organization administrator"))
    if not invitee_emails_raw:
        raise JsonableError(_("You must specify at least one email address."))

    invitee_emails = get_invitee_emails_set(invitee_emails_raw)

    streams: List[Stream] = []
    for stream_id in stream_ids:
        try:
            (stream, sub) = access_stream_by_id(user_profile, stream_id)
        except JsonableError:
            raise JsonableError(
                _("Stream does not exist with id: {}. No invites were sent.").format(stream_id)
            )
        streams.append(stream)

    if len(streams) and not user_profile.can_subscribe_other_users():
        raise JsonableError(_("You do not have permission to subscribe other users to streams."))

    do_invite_users(
        user_profile,
        invitee_emails,
        streams,
        invite_expires_in_minutes=invite_expires_in_minutes,
        invite_as=invite_as,
    )
    return json_success(request)

--- a/zerver/views/invite.py
+++ b/zerver/views/invite.py
@@ -42,6 +42,9 @@
             )
         streams.append(stream)

+    if len(streams) and not user_profile.can_subscribe_other_users():
+        raise JsonableError(_("You do not have permission to subscribe other users to streams."))
+
     do_invite_users(
         user_profile,
         invitee_emails,


The package total.js before 3.4.9 are vulnerable to Arbitrary Code Execution via the U.set() and U.get() functions.

exports.set = function(obj, path, value) {
	var cachekey = 'S+' + path;

	if (F.temporary.other[cachekey])
		return F.temporary.other[cachekey](obj, value);

	var arr = parsepath(path);
	var builder = [];

	for (var i = 0; i < arr.length - 1; i++) {
		var type = arr[i + 1] ? (REGISARR.test(arr[i + 1]) ? '[]' : '{}') : '{}';
		var p = 'w' + (arr[i][0] === '[' ? '' : '.') + arr[i];
		builder.push('if(typeof(' + p + ')!==\'object\'||' + p + '==null)' + p + '=' + type + ';');
	}

	var v = arr[arr.length - 1];
	var ispush = v.lastIndexOf('[]') !== -1;
	var a = builder.join(';') + ';var v=typeof(a)===\'function\'?a(U.get(b)):a;w' + (v[0] === '[' ? '' : '.') + (ispush ? v.replace(REGREPLACEARR, '.push(v)') : (v + '=v')) + ';return v';

	if ((/__proto__|constructor|prototype|eval/).test(a))
		throw new Error('Potential vulnerability');

	var fn = new Function('w', 'a', 'b', a);
	F.temporary.other[cachekey] = fn;
	fn(obj, value, path);
};

exports.get = function(obj, path) {

	var cachekey = 'G=' + path;

	if (F.temporary.other[cachekey])
		return F.temporary.other[cachekey](obj);

	var arr = parsepath(path);
	var builder = [];

	for (var i = 0, length = arr.length - 1; i < length; i++)
		builder.push('if(!w' + (!arr[i] || arr[i][0] === '[' ? '' : '.') + arr[i] + ')return');

	var v = arr[arr.length - 1];
	var fn = (new Function('w', builder.join(';') + ';return w' + (v[0] === '[' ? '' : '.') + v));
	F.temporary.other[cachekey] = fn;
	return fn(obj);
};

exports.set = function(obj, path, value) {
	var cachekey = 'S+' + path;

	if (F.temporary.other[cachekey])
		return F.temporary.other[cachekey](obj, value);

	if ((/__proto__|constructor|prototype|eval|function|\*|\+|;|\s|\(|\)|!/).test(path))
		return value;

	var arr = parsepath(path);
	var builder = [];

	for (var i = 0; i < arr.length - 1; i++) {
		var type = arr[i + 1] ? (REGISARR.test(arr[i + 1]) ? '[]' : '{}') : '{}';
		var p = 'w' + (arr[i][0] === '[' ? '' : '.') + arr[i];
		builder.push('if(typeof(' + p + ')!==\'object\'||' + p + '==null)' + p + '=' + type + ';');
	}

	var v = arr[arr.length - 1];
	var ispush = v.lastIndexOf('[]') !== -1;
	var a = builder.join(';') + ';var v=typeof(a)===\'function\'?a(U.get(b)):a;w' + (v[0] === '[' ? '' : '.') + (ispush ? v.replace(REGREPLACEARR, '.push(v)') : (v + '=v')) + ';return v';

	var fn = new Function('w', 'a', 'b', a);
	F.temporary.other[cachekey] = fn;
	return fn(obj, value, path);
};

exports.get = function(obj, path) {

	var cachekey = 'G=' + path;

	if (F.temporary.other[cachekey])
		return F.temporary.other[cachekey](obj);

	if ((/__proto__|constructor|prototype|eval|function|\*|\+|;|\s|\(|\)|!/).test(path))
		return;

	var arr = parsepath(path);
	var builder = [];

	for (var i = 0, length = arr.length - 1; i < length; i++)
		builder.push('if(!w' + (!arr[i] || arr[i][0] === '[' ? '' : '.') + arr[i] + ')return');

	var v = arr[arr.length - 1];
	var fn = (new Function('w', builder.join(';') + ';return w' + (v[0] === '[' ? '' : '.') + v));
	F.temporary.other[cachekey] = fn;
	return fn(obj);
};

--- a/utils.js
+++ b/utils.js
@@ -3,6 +3,9 @@

 	if (F.temporary.other[cachekey])
 		return F.temporary.other[cachekey](obj, value);
+
+	if ((/__proto__|constructor|prototype|eval|function|\*|\+|;|\s|\(|\)|!/).test(path))
+		return value;

 	var arr = parsepath(path);
 	var builder = [];
@@ -17,10 +20,7 @@
 	var ispush = v.lastIndexOf('[]') !== -1;
 	var a = builder.join(';') + ';var v=typeof(a)===\'function\'?a(U.get(b)):a;w' + (v[0] === '[' ? '' : '.') + (ispush ? v.replace(REGREPLACEARR, '.push(v)') : (v + '=v')) + ';return v';

-	if ((/__proto__|constructor|prototype|eval/).test(a))
-		throw new Error('Potential vulnerability');
-
 	var fn = new Function('w', 'a', 'b', a);
 	F.temporary.other[cachekey] = fn;
-	fn(obj, value, path);
+	return fn(obj, value, path);
 };

--- a/utils.js
+++ b/utils.js
@@ -4,6 +4,9 @@

 	if (F.temporary.other[cachekey])
 		return F.temporary.other[cachekey](obj);
+
+	if ((/__proto__|constructor|prototype|eval|function|\*|\+|;|\s|\(|\)|!/).test(path))
+		return;

 	var arr = parsepath(path);
 	var builder = [];


A flaw was found in the decompression function of registry-support. This issue can be triggered if an unauthenticated remote attacker tricks a user into parsing a devfile which uses the `parent` or `plugin` keywords. This could download a malicious archive and cause the cleanup process to overwrite or delete files outside of the archive, which should not be allowed.

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.

func decompress(targetDir string, tarFile string, excludeFiles []string) error {
	var returnedErr error

	reader, err := os.Open(filepath.Clean(tarFile))
	if err != nil {
		return err
	}

	defer func() {
		if err = reader.Close(); err != nil {
			returnedErr = multierror.Append(returnedErr, err)
		}
	}()

	gzReader, err := gzip.NewReader(reader)
	if err != nil {
		returnedErr = multierror.Append(returnedErr, err)
		return returnedErr
	}

	defer func() {
		if err = gzReader.Close(); err != nil {
			returnedErr = multierror.Append(returnedErr, err)
		}
	}()

	tarReader := tar.NewReader(gzReader)
	for {
		header, err := tarReader.Next()
		if err == io.EOF {
			break
		} else if err != nil {
			returnedErr = multierror.Append(returnedErr, err)
			return returnedErr
		}
		if isExcluded(header.Name, excludeFiles) {
			continue
		}

		target := path.Join(targetDir, filepath.Clean(header.Name))
		switch header.Typeflag {
		case tar.TypeDir:
			err = os.MkdirAll(target, os.FileMode(header.Mode))
			if err != nil {
				returnedErr = multierror.Append(returnedErr, err)
				return returnedErr
			}
		case tar.TypeReg:
			/* #nosec G304 -- target is produced using path.Join which cleans the dir path */
			w, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
			if err != nil {
				returnedErr = multierror.Append(returnedErr, err)
				return returnedErr
			}
			/* #nosec G110 -- starter projects are vetted before they are added to a registry.  Their contents can be seen before they are downloaded */
			_, err = io.Copy(w, tarReader)
			if err != nil {
				returnedErr = multierror.Append(returnedErr, err)
				return returnedErr
			}
			err = w.Close()
			if err != nil {
				returnedErr = multierror.Append(returnedErr, err)
				return returnedErr
			}
		default:
			log.Printf("Unsupported type: %v", header.Typeflag)
		}
	}

	return nil
}

func decompress(targetDir string, tarFile string, excludeFiles []string) error {
	var returnedErr error

	reader, err := os.Open(filepath.Clean(tarFile))
	if err != nil {
		return err
	}

	defer func() {
		if err = reader.Close(); err != nil {
			returnedErr = multierror.Append(returnedErr, err)
		}
	}()

	gzReader, err := gzip.NewReader(reader)
	if err != nil {
		returnedErr = multierror.Append(returnedErr, err)
		return returnedErr
	}

	defer func() {
		if err = gzReader.Close(); err != nil {
			returnedErr = multierror.Append(returnedErr, err)
		}
	}()

	tarReader := tar.NewReader(gzReader)
	for {
		header, err := tarReader.Next()
		if err == io.EOF {
			break
		} else if err != nil {
			returnedErr = multierror.Append(returnedErr, err)
			return returnedErr
		}
		if isExcluded(header.Name, excludeFiles) {
			continue
		}

		target := CleanFilepath(targetDir, header.Name)
		switch header.Typeflag {
		case tar.TypeDir:
			err = os.MkdirAll(target, os.FileMode(header.Mode))
			if err != nil {
				returnedErr = multierror.Append(returnedErr, err)
				return returnedErr
			}
		case tar.TypeReg:
			w, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
			if err != nil {
				returnedErr = multierror.Append(returnedErr, err)
				return returnedErr
			}
			/* #nosec G110 -- starter projects are vetted before they are added to a registry.  Their contents can be seen before they are downloaded */
			_, err = io.Copy(w, tarReader)
			if err != nil {
				returnedErr = multierror.Append(returnedErr, err)
				return returnedErr
			}
			err = w.Close()
			if err != nil {
				returnedErr = multierror.Append(returnedErr, err)
				return returnedErr
			}
		default:
			log.Printf("Unsupported type: %v", header.Typeflag)
		}
	}

	return nil
}

func CleanFilepath(parent string, child string)string{
	target := path.Join(parent, filepath.Clean("/"+child))
	return target
}

--- a/registry-library/library/util.go
+++ b/registry-library/library/util.go
@@ -37,7 +37,7 @@
 			continue
 		}

-		target := path.Join(targetDir, filepath.Clean(header.Name))
+		target := CleanFilepath(targetDir, header.Name)
 		switch header.Typeflag {
 		case tar.TypeDir:
 			err = os.MkdirAll(target, os.FileMode(header.Mode))
@@ -46,7 +46,6 @@
 				return returnedErr
 			}
 		case tar.TypeReg:
-			/* #nosec G304 -- target is produced using path.Join which cleans the dir path */
 			w, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
 			if err != nil {
 				returnedErr = multierror.Append(returnedErr, err)

--- /dev/null
+++ b/registry-library/library/util.go
@@ -0,0 +1,4 @@
+func CleanFilepath(parent string, child string)string{
+	target := path.Join(parent, filepath.Clean("/"+child))
+	return target
+}


SiYuan is a personal knowledge management system. Prior to version 3.1.16, an arbitrary file read vulnerability exists in Siyuan's `/api/template/render` endpoint. The absence of proper validation on the path parameter allows attackers to access sensitive files on the host system. Version 3.1.16 contains a patch for the issue.

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 renderTemplate(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(http.StatusOK, ret)

	arg, ok := util.JsonArg(c, ret)
	if !ok {
		return
	}

	p := arg["path"].(string)
	id := arg["id"].(string)
	if util.InvalidIDPattern(id, ret) {
		return
	}

	preview := false
	if previewArg := arg["preview"]; nil != previewArg {
		preview = previewArg.(bool)
	}

	_, content, err := model.RenderTemplate(p, id, preview)
	if err != nil {
		ret.Code = -1
		ret.Msg = util.EscapeHTML(err.Error())
		return
	}

	ret.Data = map[string]interface{}{
		"path":    p,
		"content": content,
	}
}

func ExportResources(resourcePaths []string, mainName string) (exportFilePath string, err error) {
	FlushTxQueue()

	// \u7528\u4e8e\u5bfc\u51fa\u7684\u4e34\u65f6\u6587\u4ef6\u5939\u5b8c\u6574\u8def\u5f84
	exportFolderPath := filepath.Join(util.TempDir, "export", mainName)
	if err = os.MkdirAll(exportFolderPath, 0755); err != nil {
		logging.LogErrorf("create export temp folder failed: %s", err)
		return
	}

	// \u5c06\u9700\u8981\u5bfc\u51fa\u7684\u6587\u4ef6/\u6587\u4ef6\u5939\u590d\u5236\u5230\u4e34\u65f6\u6587\u4ef6\u5939
	for _, resourcePath := range resourcePaths {
		resourceFullPath := filepath.Join(util.WorkspaceDir, resourcePath)    // \u8d44\u6e90\u5b8c\u6574\u8def\u5f84
		resourceBaseName := filepath.Base(resourceFullPath)                   // \u8d44\u6e90\u540d\u79f0
		resourceCopyPath := filepath.Join(exportFolderPath, resourceBaseName) // \u8d44\u6e90\u526f\u672c\u5b8c\u6574\u8def\u5f84
		if err = filelock.Copy(resourceFullPath, resourceCopyPath); err != nil {
			logging.LogErrorf("copy resource will be exported from [%s] to [%s] failed: %s", resourcePath, resourceCopyPath, err)
			err = fmt.Errorf(Conf.Language(14), err.Error())
			return
		}
	}

	zipFilePath := exportFolderPath + ".zip" // \u5bfc\u51fa\u7684 *.zip \u6587\u4ef6\u5b8c\u6574\u8def\u5f84
	zip, err := gulu.Zip.Create(zipFilePath)
	if err != nil {
		logging.LogErrorf("create export zip [%s] failed: %s", zipFilePath, err)
		return
	}

	if err = zip.AddDirectory(mainName, exportFolderPath); err != nil {
		logging.LogErrorf("create export zip [%s] failed: %s", exportFolderPath, err)
		return
	}

	if err = zip.Close(); err != nil {
		logging.LogErrorf("close export zip failed: %s", err)
	}

	os.RemoveAll(exportFolderPath)

	exportFilePath = path.Join("temp", "export", mainName+".zip") // \u5bfc\u51fa\u7684 *.zip \u6587\u4ef6\u76f8\u5bf9\u4e8e\u5de5\u4f5c\u533a\u76ee\u5f55\u7684\u8def\u5f84
	return
}

func Upload(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(200, ret)

	form, err := c.MultipartForm()
	if err != nil {
		logging.LogErrorf("insert asset failed: %s", err)
		ret.Code = -1
		ret.Msg = err.Error()
		return
	}
	assetsDirPath := filepath.Join(util.DataDir, "assets")
	if nil != form.Value["id"] {
		id := form.Value["id"][0]
		bt := treenode.GetBlockTree(id)
		if nil == bt {
			ret.Code = -1
			ret.Msg = Conf.Language(71)
			return
		}
		docDirLocalPath := filepath.Join(util.DataDir, bt.BoxID, path.Dir(bt.Path))
		assetsDirPath = getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), docDirLocalPath)
	}

	relAssetsDirPath := "assets"
	if nil != form.Value["assetsDirPath"] {
		relAssetsDirPath = form.Value["assetsDirPath"][0]
		assetsDirPath = filepath.Join(util.DataDir, relAssetsDirPath)
	}
	if !gulu.File.IsExist(assetsDirPath) {
		if err = os.MkdirAll(assetsDirPath, 0755); err != nil {
			ret.Code = -1
			ret.Msg = err.Error()
			return
		}
	}

	var errFiles []string
	succMap := map[string]interface{}{}
	files := form.File["file[]"]
	skipIfDuplicated := false // \u9ed8\u8ba4\u4e0d\u8df3\u8fc7\u91cd\u590d\u6587\u4ef6\uff0c\u4f46\u662f\u6709\u7684\u573a\u666f\u9700\u8981\u8df3\u8fc7\uff0c\u6bd4\u5982\u4e0a\u4f20 PDF \u6807\u6ce8\u56fe\u7247 https://github.com/siyuan-note/siyuan/issues/10666
	if nil != form.Value["skipIfDuplicated"] {
		skipIfDuplicated = "true" == form.Value["skipIfDuplicated"][0]
	}

	for _, file := range files {
		baseName := file.Filename

		needUnzip2Dir := false
		if gulu.OS.IsDarwin() {
			if strings.HasSuffix(baseName, ".rtfd.zip") {
				needUnzip2Dir = true
			}
		}

		fName := baseName
		fName = util.FilterUploadFileName(fName)
		ext := filepath.Ext(fName)
		fName = strings.TrimSuffix(fName, ext)
		ext = strings.ToLower(ext)
		fName += ext
		f, openErr := file.Open()
		if nil != openErr {
			errFiles = append(errFiles, fName)
			ret.Msg = openErr.Error()
			break
		}

		hash, hashErr := util.GetEtagByHandle(f, file.Size)
		if nil != hashErr {
			errFiles = append(errFiles, fName)
			ret.Msg = err.Error()
			f.Close()
			break
		}

		if existAsset := sql.QueryAssetByHash(hash); nil != existAsset {
			// \u5df2\u7ecf\u5b58\u5728\u540c\u6837\u6570\u636e\u7684\u8d44\u6e90\u6587\u4ef6\u7684\u8bdd\u4e0d\u91cd\u590d\u4fdd\u5b58
			succMap[baseName] = existAsset.Path
		} else {
			if skipIfDuplicated {
				// https://github.com/siyuan-note/siyuan/issues/10666
				matches, globErr := filepath.Glob(assetsDirPath + string(os.PathSeparator) + strings.TrimSuffix(fName, ext) + "*")
				if nil != globErr {
					logging.LogErrorf("glob failed: %s", globErr)
				} else {
					if 0 < len(matches) {
						fName = filepath.Base(matches[0])
						succMap[baseName] = strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/")
						f.Close()
						break
					}
				}
			}

			fName = util.AssetName(fName)
			writePath := filepath.Join(assetsDirPath, fName)
			tmpDir := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7))
			if needUnzip2Dir {
				if err = os.MkdirAll(tmpDir, 0755); err != nil {
					errFiles = append(errFiles, fName)
					ret.Msg = err.Error()
					f.Close()
					break
				}
				writePath = filepath.Join(tmpDir, fName)
			}

			if _, err = f.Seek(0, io.SeekStart); err != nil {
				logging.LogErrorf("seek failed: %s", err)
				errFiles = append(errFiles, fName)
				ret.Msg = err.Error()
				f.Close()
				break
			}
			if err = filelock.WriteFileByReader(writePath, f); err != nil {
				logging.LogErrorf("write file failed: %s", err)
				errFiles = append(errFiles, fName)
				ret.Msg = err.Error()
				f.Close()
				break
			}
			f.Close()

			if needUnzip2Dir {
				baseName = strings.TrimSuffix(file.Filename, ".rtfd.zip") + ".rtfd"
				fName = baseName
				fName = util.FilterUploadFileName(fName)
				ext = filepath.Ext(fName)
				fName = strings.TrimSuffix(fName, ext)
				ext = strings.ToLower(ext)
				fName += ext
				fName = util.AssetName(fName)
				tmpDir2 := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7))
				if err = gulu.Zip.Unzip(writePath, tmpDir2); err != nil {
					errFiles = append(errFiles, fName)
					ret.Msg = err.Error()
					break
				}

				entries, readErr := os.ReadDir(tmpDir2)
				if nil != readErr {
					logging.LogErrorf("read dir [%s] failed: %s", tmpDir2, readErr)
					errFiles = append(errFiles, fName)
					ret.Msg = readErr.Error()
					break
				}
				if 1 > len(entries) {
					logging.LogErrorf("read dir [%s] failed: no entry", tmpDir2)
					errFiles = append(errFiles, fName)
					ret.Msg = "no entry"
					break
				}
				dirName := entries[0].Name()
				srcDir := filepath.Join(tmpDir2, dirName)
				entries, readErr = os.ReadDir(srcDir)
				if nil != readErr {
					logging.LogErrorf("read dir [%s] failed: %s", filepath.Join(tmpDir2, entries[0].Name()), readErr)
					errFiles = append(errFiles, fName)
					ret.Msg = readErr.Error()
					break
				}
				destDir := filepath.Join(assetsDirPath, fName)
				for _, entry := range entries {
					from := filepath.Join(srcDir, entry.Name())
					to := filepath.Join(destDir, entry.Name())
					if copyErr := gulu.File.Copy(from, to); nil != copyErr {
						logging.LogErrorf("copy [%s] to [%s] failed: %s", from, to, copyErr)
						errFiles = append(errFiles, fName)
						ret.Msg = copyErr.Error()
						break
					}
				}
				os.RemoveAll(tmpDir)
				os.RemoveAll(tmpDir2)
			}

			succMap[baseName] = strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/")
		}
	}

	ret.Data = map[string]interface{}{
		"errFiles": errFiles,
		"succMap":  succMap,
	}

	IncSync()
}

func BuiltInTemplateFuncs() (ret template.FuncMap) {
	ret = sprig.TxtFuncMap()
	ret["Weekday"] = util.Weekday
	ret["WeekdayCN"] = util.WeekdayCN
	ret["WeekdayCN2"] = util.WeekdayCN2
	ret["ISOWeek"] = util.ISOWeek
	ret["pow"] = pow
	ret["powf"] = powf
	ret["log"] = log
	ret["logf"] = logf
	ret["parseTime"] = parseTime
	ret["FormatFloat"] = FormatFloat
	ret["getHPathByID"] = getHPathByID
	return
}

func renderTemplate(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(http.StatusOK, ret)

	arg, ok := util.JsonArg(c, ret)
	if !ok {
		return
	}

	p := arg["path"].(string)
	id := arg["id"].(string)
	if util.InvalidIDPattern(id, ret) {
		return
	}

	if !util.IsAbsPathInWorkspace(p) {
		ret.Code = -1
		ret.Msg = "Path [" + p + "] is not in workspace"
		return
	}

	preview := false
	if previewArg := arg["preview"]; nil != previewArg {
		preview = previewArg.(bool)
	}

	_, content, err := model.RenderTemplate(p, id, preview)
	if err != nil {
		ret.Code = -1
		ret.Msg = util.EscapeHTML(err.Error())
		return
	}

func ExportResources(resourcePaths []string, mainName string) (exportFilePath string, err error) {
	FlushTxQueue()

	// \u7528\u4e8e\u5bfc\u51fa\u7684\u4e34\u65f6\u6587\u4ef6\u5939\u5b8c\u6574\u8def\u5f84
	exportFolderPath := filepath.Join(util.TempDir, "export", mainName)
	if err = os.MkdirAll(exportFolderPath, 0755); err != nil {
		logging.LogErrorf("create export temp folder failed: %s", err)
		return
	}

	// \u5c06\u9700\u8981\u5bfc\u51fa\u7684\u6587\u4ef6/\u6587\u4ef6\u5939\u590d\u5236\u5230\u4e34\u65f6\u6587\u4ef6\u5939
	for _, resourcePath := range resourcePaths {
		resourceFullPath := filepath.Join(util.WorkspaceDir, resourcePath) // \u8d44\u6e90\u5b8c\u6574\u8def\u5f84
		if !util.IsAbsPathInWorkspace(resourceFullPath) {
			logging.LogErrorf("resource path [%s] is not in workspace", resourceFullPath)
			err = errors.New("resource path [" + resourcePath + "] is not in workspace")
			return
		}

		resourceBaseName := filepath.Base(resourceFullPath)                   // \u8d44\u6e90\u540d\u79f0
		resourceCopyPath := filepath.Join(exportFolderPath, resourceBaseName) // \u8d44\u6e90\u526f\u672c\u5b8c\u6574\u8def\u5f84
		if err = filelock.Copy(resourceFullPath, resourceCopyPath); err != nil {
			logging.LogErrorf("copy resource will be exported from [%s] to [%s] failed: %s", resourcePath, resourceCopyPath, err)
			err = fmt.Errorf(Conf.Language(14), err.Error())
			return
		}
	}

	zipFilePath := exportFolderPath + ".zip" // \u5bfc\u51fa\u7684 *.zip \u6587\u4ef6\u5b8c\u6574\u8def\u5f84
	zip, err := gulu.Zip.Create(zipFilePath)
	if err != nil {
		logging.LogErrorf("create export zip [%s] failed: %s", zipFilePath, err)
		return
	}

	if err = zip.AddDirectory(mainName, exportFolderPath); err != nil {
		logging.LogErrorf("create export zip [%s] failed: %s", exportFolderPath, err)
		return
	}

	if err = zip.Close(); err != nil {
		logging.LogErrorf("close export zip failed: %s", err)
	}

	os.RemoveAll(exportFolderPath)

	exportFilePath = path.Join("temp", "export", mainName+".zip") // \u5bfc\u51fa\u7684 *.zip \u6587\u4ef6\u76f8\u5bf9\u4e8e\u5de5\u4f5c\u533a\u76ee\u5f55\u7684\u8def\u5f84
	return
}

func Upload(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(200, ret)

	form, err := c.MultipartForm()
	if err != nil {
		logging.LogErrorf("insert asset failed: %s", err)
		ret.Code = -1
		ret.Msg = err.Error()
		return
	}
	assetsDirPath := filepath.Join(util.DataDir, "assets")
	if nil != form.Value["id"] {
		id := form.Value["id"][0]
		bt := treenode.GetBlockTree(id)
		if nil == bt {
			ret.Code = -1
			ret.Msg = Conf.Language(71)
			return
		}
		docDirLocalPath := filepath.Join(util.DataDir, bt.BoxID, path.Dir(bt.Path))
		assetsDirPath = getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), docDirLocalPath)
	}

	relAssetsDirPath := "assets"
	if nil != form.Value["assetsDirPath"] {
		relAssetsDirPath = form.Value["assetsDirPath"][0]
		assetsDirPath = filepath.Join(util.DataDir, relAssetsDirPath)
		if !util.IsAbsPathInWorkspace(assetsDirPath) {
			ret.Code = -1
			ret.Msg = "Path [" + assetsDirPath + "] is not in workspace"
			return
		}
	}
	if !gulu.File.IsExist(assetsDirPath) {
		if err = os.MkdirAll(assetsDirPath, 0755); err != nil {
			ret.Code = -1
			ret.Msg = err.Error()
			return
		}
	}

	var errFiles []string
	succMap := map[string]interface{}{}
	files := form.File["file[]"]
	skipIfDuplicated := false // \u9ed8\u8ba4\u4e0d\u8df3\u8fc7\u91cd\u590d\u6587\u4ef6\uff0c\u4f46\u662f\u6709\u7684\u573a\u666f\u9700\u8981\u8df3\u8fc7\uff0c\u6bd4\u5982\u4e0a\u4f20 PDF \u6807\u6ce8\u56fe\u7247 https://github.com/siyuan-note/siyuan/issues/10666
	if nil != form.Value["skipIfDuplicated"] {
		skipIfDuplicated = "true" == form.Value["skipIfDuplicated"][0]
	}

	for _, file := range files {
		baseName := file.Filename

		needUnzip2Dir := false
		if gulu.OS.IsDarwin() {
			if strings.HasSuffix(baseName, ".rtfd.zip") {
				needUnzip2Dir = true
			}
		}

		fName := baseName
		fName = util.FilterUploadFileName(fName)
		ext := filepath.Ext(fName)
		fName = strings.TrimSuffix(fName, ext)
		ext = strings.ToLower(ext)
		fName += ext
		f, openErr := file.Open()
		if nil != openErr {
			errFiles = append(errFiles, fName)
			ret.Msg = openErr.Error()
			break
		}

		hash, hashErr := util.GetEtagByHandle(f, file.Size)
		if nil != hashErr {
			errFiles = append(errFiles, fName)
			ret.Msg = err.Error()
			f.Close()
			break
		}

		if existAsset := sql.QueryAssetByHash(hash); nil != existAsset {
			// \u5df2\u7ecf\u5b58\u5728\u540c\u6837\u6570\u636e\u7684\u8d44\u6e90\u6587\u4ef6\u7684\u8bdd\u4e0d\u91cd\u590d\u4fdd\u5b58
			succMap[baseName] = existAsset.Path
		} else {
			if skipIfDuplicated {
				// https://github.com/siyuan-note/siyuan/issues/10666
				matches, globErr := filepath.Glob(assetsDirPath + string(os.PathSeparator) + strings.TrimSuffix(fName, ext) + "*")
				if nil != globErr {
					logging.LogErrorf("glob failed: %s", globErr)
				} else {
					if 0 < len(matches) {
						fName = filepath.Base(matches[0])
						succMap[baseName] = strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/")
						f.Close()
						break
					}
				}
			}

			fName = util.AssetName(fName)
			writePath := filepath.Join(assetsDirPath, fName)
			tmpDir := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7))
			if needUnzip2Dir {
				if err = os.MkdirAll(tmpDir, 0755); err != nil {
					errFiles = append(errFiles, fName)
					ret.Msg = err.Error()
					f.Close()
					break
				}
				writePath = filepath.Join(tmpDir, fName)
			}

			if _, err = f.Seek(0, io.SeekStart); err != nil {
				logging.LogErrorf("seek failed: %s", err)
				errFiles = append(errFiles, fName)
				ret.Msg = err.Error()
				f.Close()
				break
			}
			if err = filelock.WriteFileByReader(writePath, f); err != nil {
				logging.LogErrorf("write file failed: %s", err)
				errFiles = append(errFiles, fName)
				ret.Msg = err.Error()
				f.Close()
				break
			}
			f.Close()

			if needUnzip2Dir {
				baseName = strings.TrimSuffix(file.Filename, ".rtfd.zip") + ".rtfd"
				fName = baseName
				fName = util.FilterUploadFileName(fName)
				ext = filepath.Ext(fName)
				fName = strings.TrimSuffix(fName, ext)
				ext = strings.ToLower(ext)
				fName += ext
				fName = util.AssetName(fName)
				tmpDir2 := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7))
				if err = gulu.Zip.Unzip(writePath, tmpDir2); err != nil {
					errFiles = append(errFiles, fName)
					ret.Msg = err.Error()
					break
				}

				entries, readErr := os.ReadDir(tmpDir2)
				if nil != readErr {
					logging.LogErrorf("read dir [%s] failed: %s", tmpDir2, readErr)
					errFiles = append(errFiles, fName)
					ret.Msg = readErr.Error()
					break
				}
				if 1 > len(entries) {
					logging.LogErrorf("read dir [%s] failed: no entry", tmpDir2)
					errFiles = append(errFiles, fName)
					ret.Msg = "no entry"
					break
				}
				dirName := entries[0].Name()
				srcDir := filepath.Join(tmpDir2, dirName)
				entries, readErr = os.ReadDir(srcDir)
				if nil != readErr {
					logging.LogErrorf("read dir [%s] failed: %s", filepath.Join(tmpDir2, entries[0].Name()), readErr)
					errFiles = append(errFiles, fName)
					ret.Msg = readErr.Error()
					break
				}
				destDir := filepath.Join(assetsDirPath, fName)
				for _, entry := range entries {
					from := filepath.Join(srcDir, entry.Name())
					to := filepath.Join(destDir, entry.Name())
					if copyErr := gulu.File.Copy(from, to); nil != copyErr {
						logging.LogErrorf("copy [%s] to [%s] failed: %s", from, to, copyErr)
						errFiles = append(errFiles, fName)
						ret.Msg = copyErr.Error()
						break
					}
				}
				os.RemoveAll(tmpDir)
				os.RemoveAll(tmpDir2)
			}

			succMap[baseName] = strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/")
		}
	}

	ret.Data = map[string]interface{}{
		"errFiles": errFiles,
		"succMap":  succMap,
	}

	IncSync()
}

func BuiltInTemplateFuncs() (ret template.FuncMap) {
	ret = sprig.TxtFuncMap()

	// \u56e0\u4e3a\u5b89\u5168\u539f\u56e0\u79fb\u9664\u4e00\u4e9b\u51fd\u6570 https://github.com/siyuan-note/siyuan/issues/13426
	delete(ret, "env")
	delete(ret, "expandenv")
	delete(ret, "getHostByName")

	ret["Weekday"] = util.Weekday
	ret["WeekdayCN"] = util.WeekdayCN
	ret["WeekdayCN2"] = util.WeekdayCN2
	ret["ISOWeek"] = util.ISOWeek
	ret["pow"] = pow
	ret["powf"] = powf
	ret["log"] = log
	ret["logf"] = logf
	ret["parseTime"] = parseTime
	ret["FormatFloat"] = FormatFloat
	ret["getHPathByID"] = getHPathByID
	return
}

func IsAbsPathInWorkspace(absPath string) bool {
	return IsSubPath(WorkspaceDir, absPath)
}

--- a/kernel/api/template.go
+++ b/kernel/api/template.go
@@ -13,6 +13,12 @@
 		return
 	}

+	if !util.IsAbsPathInWorkspace(p) {
+		ret.Code = -1
+		ret.Msg = "Path [" + p + "] is not in workspace"
+		return
+	}
+
 	preview := false
 	if previewArg := arg["preview"]; nil != previewArg {
 		preview = previewArg.(bool)
@@ -24,9 +30,3 @@
 		ret.Msg = util.EscapeHTML(err.Error())
 		return
 	}
-
-	ret.Data = map[string]interface{}{
-		"path":    p,
-		"content": content,
-	}
-}

--- a/kernel/model/export.go
+++ b/kernel/model/export.go
@@ -10,7 +10,13 @@

 	// \u5c06\u9700\u8981\u5bfc\u51fa\u7684\u6587\u4ef6/\u6587\u4ef6\u5939\u590d\u5236\u5230\u4e34\u65f6\u6587\u4ef6\u5939
 	for _, resourcePath := range resourcePaths {
-		resourceFullPath := filepath.Join(util.WorkspaceDir, resourcePath)    // \u8d44\u6e90\u5b8c\u6574\u8def\u5f84
+		resourceFullPath := filepath.Join(util.WorkspaceDir, resourcePath) // \u8d44\u6e90\u5b8c\u6574\u8def\u5f84
+		if !util.IsAbsPathInWorkspace(resourceFullPath) {
+			logging.LogErrorf("resource path [%s] is not in workspace", resourceFullPath)
+			err = errors.New("resource path [" + resourcePath + "] is not in workspace")
+			return
+		}
+
 		resourceBaseName := filepath.Base(resourceFullPath)                   // \u8d44\u6e90\u540d\u79f0
 		resourceCopyPath := filepath.Join(exportFolderPath, resourceBaseName) // \u8d44\u6e90\u526f\u672c\u5b8c\u6574\u8def\u5f84
 		if err = filelock.Copy(resourceFullPath, resourceCopyPath); err != nil {

--- a/kernel/model/upload.go
+++ b/kernel/model/upload.go
@@ -26,6 +26,11 @@
 	if nil != form.Value["assetsDirPath"] {
 		relAssetsDirPath = form.Value["assetsDirPath"][0]
 		assetsDirPath = filepath.Join(util.DataDir, relAssetsDirPath)
+		if !util.IsAbsPathInWorkspace(assetsDirPath) {
+			ret.Code = -1
+			ret.Msg = "Path [" + assetsDirPath + "] is not in workspace"
+			return
+		}
 	}
 	if !gulu.File.IsExist(assetsDirPath) {
 		if err = os.MkdirAll(assetsDirPath, 0755); err != nil {

--- a/kernel/treenode/template.go
+++ b/kernel/treenode/template.go
@@ -1,5 +1,11 @@
 func BuiltInTemplateFuncs() (ret template.FuncMap) {
 	ret = sprig.TxtFuncMap()
+
+	// \u56e0\u4e3a\u5b89\u5168\u539f\u56e0\u79fb\u9664\u4e00\u4e9b\u51fd\u6570 https://github.com/siyuan-note/siyuan/issues/13426
+	delete(ret, "env")
+	delete(ret, "expandenv")
+	delete(ret, "getHostByName")
+
 	ret["Weekday"] = util.Weekday
 	ret["WeekdayCN"] = util.WeekdayCN
 	ret["WeekdayCN2"] = util.WeekdayCN2

--- /dev/null
+++ b/kernel/treenode/template.go
@@ -0,0 +1,3 @@
+func IsAbsPathInWorkspace(absPath string) bool {
+	return IsSubPath(WorkspaceDir, absPath)
+}


A vulnerability was found in zwczou WeChat SDK Python 0.3.0 and classified as critical. This issue affects the function validate/to_xml. The manipulation leads to xml external entity reference. The attack may be initiated remotely. Upgrading to version 0.5.5 is able to address this issue. The patch is named e54abadc777715b6dcb545c13214d1dea63df6c9. It is recommended to upgrade the affected component. The associated identifier of this vulnerability is VDB-223403.

Improper Restriction of XML External Entity Reference

The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.

    def parse(self, content):
        raw = {}
        root = etree.fromstring(content)
        for child in root:
            raw[child.tag] = child.text

        formatted = self.format(raw)
        msg_type = formatted['type']
        msg_parser = getattr(self, 'parse_{0}'.format(msg_type), None)
        if callable(msg_parser):
            parsed = msg_parser(raw)
        else:
            parsed = self.parse_invalid_type(raw)

        formatted.update(parsed)
        return formatted

    def to_dict(self, content):
        raw = {}
        root = etree.fromstring(content.encode("utf-8"))
        for child in root:
            raw[child.tag] = child.text
        return raw

    def parse(self, content):
        raw = {}
        root = etree.fromstring(content,
                                parser=etree.XMLParser(resolve_entities=False))
        for child in root:
            raw[child.tag] = child.text

        formatted = self.format(raw)
        msg_type = formatted['type']
        msg_parser = getattr(self, 'parse_{0}'.format(msg_type), None)
        if callable(msg_parser):
            parsed = msg_parser(raw)
        else:
            parsed = self.parse_invalid_type(raw)

        formatted.update(parsed)
        return formatted

    def to_dict(self, content):
        raw = {}
        root = etree.fromstring(content.encode("utf-8"),
                                parser=etree.XMLParser(resolve_entities=False))
        for child in root:
            raw[child.tag] = child.text
        return raw

--- a/weixin/msg.py
+++ b/weixin/msg.py
@@ -1,6 +1,7 @@
     def parse(self, content):
         raw = {}
-        root = etree.fromstring(content)
+        root = etree.fromstring(content,
+                                parser=etree.XMLParser(resolve_entities=False))
         for child in root:
             raw[child.tag] = child.text

--- a/weixin/pay.py
+++ b/weixin/pay.py
@@ -1,6 +1,7 @@
     def to_dict(self, content):
         raw = {}
-        root = etree.fromstring(content.encode("utf-8"))
+        root = etree.fromstring(content.encode("utf-8"),
+                                parser=etree.XMLParser(resolve_entities=False))
         for child in root:
             raw[child.tag] = child.text
         return raw


A Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') vulnerability in SUSE Rancher allows code execution for user with the ability to add an untrusted Helm catalog or modifying the URL configuration used to download KDM (only admin users by default) This issue affects: SUSE Rancher Rancher versions prior to 2.5.17; Rancher versions prior to 2.6.10; Rancher versions prior to 2.7.1.

func Clone(path, url, branch string) error {
	if err := ValidateURL(url); err != nil {
		return err
	}
	return runcmd("git", "clone", "-b", branch, "--single-branch", url, path)
}

func RemoteBranchHeadCommit(url, branch string) (string, error) {
	if err := ValidateURL(url); err != nil {
		return "", err
	}
	cmd := exec.Command("git", "ls-remote", url, branch)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return "", errors.Wrap(err, string(output))
	}
	parts := strings.Split(string(output), "\t")
	if len(parts) > 0 && len(parts[0]) > 0 {
		return parts[0], nil
	}
	return "", fmt.Errorf("no commit found for url %s branch %s", branch, url)
}

func IsValid(url string) bool {
	if err := ValidateURL(url); err != nil {
		return false
	}
	err := runcmd("git", "ls-remote", url)
	return err == nil
}

func CloneWithDepth(path, url, branch string, depth int) error {
	if err := ValidateURL(url); err != nil {
		return err
	}
	return runcmd("git", "clone", "-b", branch, "--single-branch", fmt.Sprintf("--depth=%v", depth), url, path)
}

func Clone(path, url, branch string) error {
	if err := ValidateURL(url); err != nil {
		return err
	}
	return runcmd("git", "clone", "-b", branch, "--single-branch", "--", url, path)
}

func RemoteBranchHeadCommit(url, branch string) (string, error) {
	if err := ValidateURL(url); err != nil {
		return "", err
	}
	cmd := exec.Command("git", "ls-remote", "--", url, branch)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return "", errors.Wrap(err, string(output))
	}
	parts := strings.Split(string(output), "\t")
	if len(parts) > 0 && len(parts[0]) > 0 {
		return parts[0], nil
	}
	return "", fmt.Errorf("no commit found for url %s branch %s", branch, url)
}

func IsValid(url string) bool {
	if err := ValidateURL(url); err != nil {
		return false
	}
	err := runcmd("git", "ls-remote", "--", url)
	return err == nil
}

func CloneWithDepth(path, url, branch string, depth int) error {
	if err := ValidateURL(url); err != nil {
		return err
	}
	return runcmd("git", "clone", "-b", branch, "--single-branch", fmt.Sprintf("--depth=%v", depth), "--", url, path)
}

--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -2,5 +2,5 @@
 	if err := ValidateURL(url); err != nil {
 		return err
 	}
-	return runcmd("git", "clone", "-b", branch, "--single-branch", url, path)
+	return runcmd("git", "clone", "-b", branch, "--single-branch", "--", url, path)
 }

--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -2,7 +2,7 @@
 	if err := ValidateURL(url); err != nil {
 		return "", err
 	}
-	cmd := exec.Command("git", "ls-remote", url, branch)
+	cmd := exec.Command("git", "ls-remote", "--", url, branch)
 	output, err := cmd.CombinedOutput()
 	if err != nil {
 		return "", errors.Wrap(err, string(output))

--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -2,6 +2,6 @@
 	if err := ValidateURL(url); err != nil {
 		return false
 	}
-	err := runcmd("git", "ls-remote", url)
+	err := runcmd("git", "ls-remote", "--", url)
 	return err == nil
 }

--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -2,5 +2,5 @@
 	if err := ValidateURL(url); err != nil {
 		return err
 	}
-	return runcmd("git", "clone", "-b", branch, "--single-branch", fmt.Sprintf("--depth=%v", depth), url, path)
+	return runcmd("git", "clone", "-b", branch, "--single-branch", fmt.Sprintf("--depth=%v", depth), "--", url, path)
 }


SQL injection vulnerability in ARAX-UI Synonym Lookup functionality in GitHub repository rtxteam/rtx prior to checkpoint_2022-04-20 . This vulnerability is critical as it can lead to remote code execution and thus complete server takeover.

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

The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.

def get_nodes_like(word,requested_limit):

    debug = False

    t0 = timeit.default_timer()
    requested_limit = int(requested_limit)

    values = []
    n_values = 0

    if len(word) < 2:
        return values

    floor = word[:-1]
    ceiling = floor + 'zz'

    #### Get a list of matching node names that begin with these letters
    if debug:
        print(f"INFO: Query 1")
    #cursor.execute("SELECT term FROM term WHERE term LIKE \"%s%%\" ORDER BY length(term),term LIMIT %s" % (word,1000))
    cursor.execute(f"SELECT term FROM terms WHERE term > \"{floor}\" AND term < \"{ceiling}\" AND term LIKE \"{word}%%\" ORDER BY length(term),term LIMIT {requested_limit}")
    rows = cursor.fetchall()
    values_dict = {}
    for row in rows:
        term = row[0]
        if term.upper() not in values_dict:
            if debug:
                print(f"    - {term}")
            properties = { "curie": '??', "name": term, "type": '??' }
            values.append(properties)
            values_dict[term.upper()] = 1
            n_values += 1
            if n_values >= requested_limit:
                break
    t1 = timeit.default_timer()
    if debug:
        print(f"INFO: Query 1 in {t1-t0} sec")

    #### If we haven't reached the limit yet, add a list of matching terms that contain this string
    if n_values < requested_limit:
        if debug:
            print(f"INFO: Query 2")

        #### See if there is a cached entry already
        word_part = word
        found_fragment = None
        while len(word_part) > 2:
            cursor.execute(f"SELECT rowid, fragment FROM cached_fragments WHERE fragment == \"{word_part}\"")
            rows = cursor.fetchall()
            if len(rows) > 0:
                fragment_id = rows[0][0]
                found_fragment = rows[0][1]
                break
            word_part = word_part[:-1]

        if found_fragment:
            if debug:
                print(f"Found matching fragment {found_fragment} as fragment_id {fragment_id}")

            cursor.execute(f"SELECT term FROM cached_fragment_terms WHERE fragment_id = {fragment_id} AND term LIKE \"%%{word}%%\"")
            rows = cursor.fetchall()

            for row in rows:
                term = row[0]
                if term.upper() not in values_dict:

                    if n_values < requested_limit:
                        if debug:
                            print(f"    - {term}")
                        properties = { "curie": '??', "name": term, "type": '??' }
                        values.append(properties)
                        n_values += 1

        if found_fragment is None:

            #### Cache this fragment in the database
            cursor.execute("INSERT INTO cached_fragments(fragment) VALUES(?)", (word,))
            fragment_id = cursor.lastrowid
            if debug:
                print(f"fragment_id = {fragment_id}")

            #### Execute an expensive LIKE query
            cursor.execute("SELECT term FROM terms WHERE term LIKE \"%%%s%%\" ORDER BY length(term),term LIMIT %s" % (word,10000))
            rows = cursor.fetchall()

            for row in rows:
                term = row[0]
                if term.upper() not in values_dict:

                    if n_values < requested_limit:
                        if debug:
                            print(f"    - {term}")
                        properties = { "curie": '??', "name": term, "type": '??' }
                        values.append(properties)
                        n_values += 1

                    values_dict[term.upper()] = 1
                    cursor.execute("INSERT INTO cached_fragment_terms(fragment_id, term) VALUES(?,?)", (fragment_id, term,))
            conn.commit()

        t2 = timeit.default_timer()
        if debug:
            print(f"INFO: Query 2 in {t2-t1} sec")

    return(values)

def get_nodes_like(word,requested_limit):

    debug = False

    t0 = timeit.default_timer()
    requested_limit = int(requested_limit)

    values = []
    n_values = 0

    if len(word) < 2:
        return values

    #### Try to avoid SQL injection exploits by sanitizing input #1823
    word = word.replace('"','')

    floor = word[:-1]
    ceiling = floor + 'zz'

    #### Get a list of matching node names that begin with these letters
    if debug:
        print(f"INFO: Query 1")
    #cursor.execute("SELECT term FROM term WHERE term LIKE \"%s%%\" ORDER BY length(term),term LIMIT %s" % (word,1000))
    cursor.execute(f"SELECT term FROM terms WHERE term > \"{floor}\" AND term < \"{ceiling}\" AND term LIKE \"{word}%%\" ORDER BY length(term),term LIMIT {requested_limit}")
    rows = cursor.fetchall()
    values_dict = {}
    for row in rows:
        term = row[0]
        if term.upper() not in values_dict:
            if debug:
                print(f"    - {term}")
            properties = { "curie": '??', "name": term, "type": '??' }
            values.append(properties)
            values_dict[term.upper()] = 1
            n_values += 1
            if n_values >= requested_limit:
                break
    t1 = timeit.default_timer()
    if debug:
        print(f"INFO: Query 1 in {t1-t0} sec")

    #### If we haven't reached the limit yet, add a list of matching terms that contain this string
    if n_values < requested_limit:
        if debug:
            print(f"INFO: Query 2")

        #### See if there is a cached entry already
        word_part = word
        found_fragment = None
        while len(word_part) > 2:
            cursor.execute(f"SELECT rowid, fragment FROM cached_fragments WHERE fragment == \"{word_part}\"")
            rows = cursor.fetchall()
            if len(rows) > 0:
                fragment_id = rows[0][0]
                found_fragment = rows[0][1]
                break
            word_part = word_part[:-1]

        if found_fragment:
            if debug:
                print(f"Found matching fragment {found_fragment} as fragment_id {fragment_id}")

            cursor.execute(f"SELECT term FROM cached_fragment_terms WHERE fragment_id = {fragment_id} AND term LIKE \"%%{word}%%\"")
            rows = cursor.fetchall()

            for row in rows:
                term = row[0]
                if term.upper() not in values_dict:

                    if n_values < requested_limit:
                        if debug:
                            print(f"    - {term}")
                        properties = { "curie": '??', "name": term, "type": '??' }
                        values.append(properties)
                        n_values += 1

        if found_fragment is None:

            #### Cache this fragment in the database
            try:
                cursor.execute("INSERT INTO cached_fragments(fragment) VALUES(?)", (word,))
                fragment_id = cursor.lastrowid
            except:
                print(f"ERROR: Unable to INSERT into cached_fragments(fragment)",file=sys.stderr)
                fragment_id = 0
            if debug:
                print(f"fragment_id = {fragment_id}")

            #### Execute an expensive LIKE query
            cursor.execute("SELECT term FROM terms WHERE term LIKE \"%%%s%%\" ORDER BY length(term),term LIMIT %s" % (word,10000))
            rows = cursor.fetchall()

            for row in rows:
                term = row[0]
                if term.upper() not in values_dict:

                    if n_values < requested_limit:
                        if debug:
                            print(f"    - {term}")
                        properties = { "curie": '??', "name": term, "type": '??' }
                        values.append(properties)
                        n_values += 1

                    values_dict[term.upper()] = 1
                    cursor.execute("INSERT INTO cached_fragment_terms(fragment_id, term) VALUES(?,?)", (fragment_id, term,))
            conn.commit()

        t2 = timeit.default_timer()
        if debug:
            print(f"INFO: Query 2 in {t2-t1} sec")

    return(values)

--- a/code/autocomplete/rtxcomplete.py
+++ b/code/autocomplete/rtxcomplete.py
@@ -10,6 +10,9 @@

     if len(word) < 2:
         return values
+
+    #### Try to avoid SQL injection exploits by sanitizing input #1823
+    word = word.replace('"','')

     floor = word[:-1]
     ceiling = floor + 'zz'
@@ -75,8 +78,12 @@
         if found_fragment is None:

             #### Cache this fragment in the database
-            cursor.execute("INSERT INTO cached_fragments(fragment) VALUES(?)", (word,))
-            fragment_id = cursor.lastrowid
+            try:
+                cursor.execute("INSERT INTO cached_fragments(fragment) VALUES(?)", (word,))
+                fragment_id = cursor.lastrowid
+            except:
+                print(f"ERROR: Unable to INSERT into cached_fragments(fragment)",file=sys.stderr)
+                fragment_id = 0
             if debug:
                 print(f"fragment_id = {fragment_id}")


A Server-Side Request Forgery (SSRF) vulnerability exists in berriai/litellm version 1.38.10. This vulnerability allows users to specify the `api_base` parameter when making requests to `POST /chat/completions`, causing the application to send the request to the domain specified by `api_base`. This request includes the OpenAI API key. A malicious user can set the `api_base` to their own domain and intercept the OpenAI API key, leading to unauthorized access and potential misuse of the API key.

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.

def common_checks(
    request_body: dict,
    team_object: Optional[LiteLLM_TeamTable],
    user_object: Optional[LiteLLM_UserTable],
    end_user_object: Optional[LiteLLM_EndUserTable],
    global_proxy_spend: Optional[float],
    general_settings: dict,
    route: str,
) -> bool:
    """
    Common checks across jwt + key-based auth.

    1. If team is blocked
    2. If team can call model
    3. If team is in budget
    4. If user passed in (JWT or key.user_id) - is in budget
    5. If end_user (either via JWT or 'user' passed to /chat/completions, /embeddings endpoint) is in budget
    6. [OPTIONAL] If 'enforce_end_user' enabled - did developer pass in 'user' param for openai endpoints
    7. [OPTIONAL] If 'litellm.max_budget' is set (>0), is proxy under budget
    8. [OPTIONAL] If guardrails modified - is request allowed to change this
    """
    _model = request_body.get("model", None)
    if team_object is not None and team_object.blocked is True:
        raise Exception(
            f"Team={team_object.team_id} is blocked. Update via `/team/unblock` if your admin."
        )
    # 2. If team can call model
    if (
        _model is not None
        and team_object is not None
        and len(team_object.models) > 0
        and _model not in team_object.models
    ):
        # this means the team has access to all models on the proxy
        if (
            "all-proxy-models" in team_object.models
            or "*" in team_object.models
            or "openai/*" in team_object.models
        ):
            # this means the team has access to all models on the proxy
            pass
        # check if the team model is an access_group
        elif model_in_access_group(_model, team_object.models) is True:
            pass
        elif _model and "*" in _model:
            pass
        else:
            raise Exception(
                f"Team={team_object.team_id} not allowed to call model={_model}. Allowed team models = {team_object.models}"
            )
    # 3. If team is in budget
    if (
        team_object is not None
        and team_object.max_budget is not None
        and team_object.spend is not None
        and team_object.spend > team_object.max_budget
    ):
        raise litellm.BudgetExceededError(
            current_cost=team_object.spend,
            max_budget=team_object.max_budget,
            message=f"Team={team_object.team_id} over budget. Spend={team_object.spend}, Budget={team_object.max_budget}",
        )
    # 4. If user is in budget
    ## 4.1 check personal budget, if personal key
    if (
        (team_object is None or team_object.team_id is None)
        and user_object is not None
        and user_object.max_budget is not None
    ):
        user_budget = user_object.max_budget
        if user_budget < user_object.spend:
            raise litellm.BudgetExceededError(
                current_cost=user_object.spend,
                max_budget=user_budget,
                message=f"ExceededBudget: User={user_object.user_id} over budget. Spend={user_object.spend}, Budget={user_budget}",
            )
    ## 4.2 check team member budget, if team key
    # 5. If end_user ('user' passed to /chat/completions, /embeddings endpoint) is in budget
    if end_user_object is not None and end_user_object.litellm_budget_table is not None:
        end_user_budget = end_user_object.litellm_budget_table.max_budget
        if end_user_budget is not None and end_user_object.spend > end_user_budget:
            raise litellm.BudgetExceededError(
                current_cost=end_user_object.spend,
                max_budget=end_user_budget,
                message=f"ExceededBudget: End User={end_user_object.user_id} over budget. Spend={end_user_object.spend}, Budget={end_user_budget}",
            )
    # 6. [OPTIONAL] If 'enforce_user_param' enabled - did developer pass in 'user' param for openai endpoints
    if (
        general_settings.get("enforce_user_param", None) is not None
        and general_settings["enforce_user_param"] == True
    ):
        if is_llm_api_route(route=route) and "user" not in request_body:
            raise Exception(
                f"'user' param not passed in. 'enforce_user_param'={general_settings['enforce_user_param']}"
            )
    if general_settings.get("enforced_params", None) is not None:
        # Enterprise ONLY Feature
        # we already validate if user is premium_user when reading the config
        # Add an extra premium_usercheck here too, just incase
        from litellm.proxy.proxy_server import CommonProxyErrors, premium_user

        if premium_user is not True:
            raise ValueError(
                "Trying to use `enforced_params`"
                + CommonProxyErrors.not_premium_user.value
            )

        if is_llm_api_route(route=route):
            # loop through each enforced param
            # example enforced_params ['user', 'metadata', 'metadata.generation_name']
            for enforced_param in general_settings["enforced_params"]:
                _enforced_params = enforced_param.split(".")
                if len(_enforced_params) == 1:
                    if _enforced_params[0] not in request_body:
                        raise ValueError(
                            f"BadRequest please pass param={_enforced_params[0]} in request body. This is a required param"
                        )
                elif len(_enforced_params) == 2:
                    # this is a scenario where user requires request['metadata']['generation_name'] to exist
                    if _enforced_params[0] not in request_body:
                        raise ValueError(
                            f"BadRequest please pass param={_enforced_params[0]} in request body. This is a required param"
                        )
                    if _enforced_params[1] not in request_body[_enforced_params[0]]:
                        raise ValueError(
                            f"BadRequest please pass param=[{_enforced_params[0]}][{_enforced_params[1]}] in request body. This is a required param"
                        )

        pass
    # 7. [OPTIONAL] If 'litellm.max_budget' is set (>0), is proxy under budget
    if (
        litellm.max_budget > 0
        and global_proxy_spend is not None
        # only run global budget checks for OpenAI routes
        # Reason - the Admin UI should continue working if the proxy crosses it's global budget
        and is_llm_api_route(route=route)
        and route != "/v1/models"
        and route != "/models"
    ):
        if global_proxy_spend > litellm.max_budget:
            raise litellm.BudgetExceededError(
                current_cost=global_proxy_spend, max_budget=litellm.max_budget
            )

    _request_metadata: dict = request_body.get("metadata", {}) or {}
    if _request_metadata.get("guardrails"):
        # check if team allowed to modify guardrails
        from litellm.proxy.guardrails.guardrail_helpers import can_modify_guardrails

        can_modify: bool = can_modify_guardrails(team_object)
        if can_modify is False:
            from fastapi import HTTPException

            raise HTTPException(
                status_code=403,
                detail={
                    "error": "Your team does not have permission to modify guardrails."
                },
            )
    return True

def is_request_body_safe(request_body: dict) -> bool:
    """
    Check if the request body is safe.

    A malicious user can set the \ufeffapi_base to their own domain and invoke POST /chat/completions to intercept and steal the OpenAI API key.
    Relevant issue: https://huntr.com/bounties/4001e1a2-7b7a-4776-a3ae-e6692ec3d997
    """
    banned_params = ["api_base", "base_url"]

    for param in banned_params:
        if param in request_body:
            raise ValueError(f"BadRequest: {param} is not allowed in request body")

    return True

def common_checks(
    request_body: dict,
    team_object: Optional[LiteLLM_TeamTable],
    user_object: Optional[LiteLLM_UserTable],
    end_user_object: Optional[LiteLLM_EndUserTable],
    global_proxy_spend: Optional[float],
    general_settings: dict,
    route: str,
) -> bool:
    """
    Common checks across jwt + key-based auth.

    1. If team is blocked
    2. If team can call model
    3. If team is in budget
    4. If user passed in (JWT or key.user_id) - is in budget
    5. If end_user (either via JWT or 'user' passed to /chat/completions, /embeddings endpoint) is in budget
    6. [OPTIONAL] If 'enforce_end_user' enabled - did developer pass in 'user' param for openai endpoints
    7. [OPTIONAL] If 'litellm.max_budget' is set (>0), is proxy under budget
    8. [OPTIONAL] If guardrails modified - is request allowed to change this
    9. Check if request body is safe
    """
    _model = request_body.get("model", None)
    if team_object is not None and team_object.blocked is True:
        raise Exception(
            f"Team={team_object.team_id} is blocked. Update via `/team/unblock` if your admin."
        )
    # 2. If team can call model
    if (
        _model is not None
        and team_object is not None
        and len(team_object.models) > 0
        and _model not in team_object.models
    ):
        # this means the team has access to all models on the proxy
        if (
            "all-proxy-models" in team_object.models
            or "*" in team_object.models
            or "openai/*" in team_object.models
        ):
            # this means the team has access to all models on the proxy
            pass
        # check if the team model is an access_group
        elif model_in_access_group(_model, team_object.models) is True:
            pass
        elif _model and "*" in _model:
            pass
        else:
            raise Exception(
                f"Team={team_object.team_id} not allowed to call model={_model}. Allowed team models = {team_object.models}"
            )
    # 3. If team is in budget
    if (
        team_object is not None
        and team_object.max_budget is not None
        and team_object.spend is not None
        and team_object.spend > team_object.max_budget
    ):
        raise litellm.BudgetExceededError(
            current_cost=team_object.spend,
            max_budget=team_object.max_budget,
            message=f"Team={team_object.team_id} over budget. Spend={team_object.spend}, Budget={team_object.max_budget}",
        )
    # 4. If user is in budget
    ## 4.1 check personal budget, if personal key
    if (
        (team_object is None or team_object.team_id is None)
        and user_object is not None
        and user_object.max_budget is not None
    ):
        user_budget = user_object.max_budget
        if user_budget < user_object.spend:
            raise litellm.BudgetExceededError(
                current_cost=user_object.spend,
                max_budget=user_budget,
                message=f"ExceededBudget: User={user_object.user_id} over budget. Spend={user_object.spend}, Budget={user_budget}",
            )
    ## 4.2 check team member budget, if team key
    # 5. If end_user ('user' passed to /chat/completions, /embeddings endpoint) is in budget
    if end_user_object is not None and end_user_object.litellm_budget_table is not None:
        end_user_budget = end_user_object.litellm_budget_table.max_budget
        if end_user_budget is not None and end_user_object.spend > end_user_budget:
            raise litellm.BudgetExceededError(
                current_cost=end_user_object.spend,
                max_budget=end_user_budget,
                message=f"ExceededBudget: End User={end_user_object.user_id} over budget. Spend={end_user_object.spend}, Budget={end_user_budget}",
            )
    # 6. [OPTIONAL] If 'enforce_user_param' enabled - did developer pass in 'user' param for openai endpoints
    if (
        general_settings.get("enforce_user_param", None) is not None
        and general_settings["enforce_user_param"] == True
    ):
        if is_llm_api_route(route=route) and "user" not in request_body:
            raise Exception(
                f"'user' param not passed in. 'enforce_user_param'={general_settings['enforce_user_param']}"
            )
    if general_settings.get("enforced_params", None) is not None:
        # Enterprise ONLY Feature
        # we already validate if user is premium_user when reading the config
        # Add an extra premium_usercheck here too, just incase
        from litellm.proxy.proxy_server import CommonProxyErrors, premium_user

        if premium_user is not True:
            raise ValueError(
                "Trying to use `enforced_params`"
                + CommonProxyErrors.not_premium_user.value
            )

        if is_llm_api_route(route=route):
            # loop through each enforced param
            # example enforced_params ['user', 'metadata', 'metadata.generation_name']
            for enforced_param in general_settings["enforced_params"]:
                _enforced_params = enforced_param.split(".")
                if len(_enforced_params) == 1:
                    if _enforced_params[0] not in request_body:
                        raise ValueError(
                            f"BadRequest please pass param={_enforced_params[0]} in request body. This is a required param"
                        )
                elif len(_enforced_params) == 2:
                    # this is a scenario where user requires request['metadata']['generation_name'] to exist
                    if _enforced_params[0] not in request_body:
                        raise ValueError(
                            f"BadRequest please pass param={_enforced_params[0]} in request body. This is a required param"
                        )
                    if _enforced_params[1] not in request_body[_enforced_params[0]]:
                        raise ValueError(
                            f"BadRequest please pass param=[{_enforced_params[0]}][{_enforced_params[1]}] in request body. This is a required param"
                        )

        pass
    # 7. [OPTIONAL] If 'litellm.max_budget' is set (>0), is proxy under budget
    if (
        litellm.max_budget > 0
        and global_proxy_spend is not None
        # only run global budget checks for OpenAI routes
        # Reason - the Admin UI should continue working if the proxy crosses it's global budget
        and is_llm_api_route(route=route)
        and route != "/v1/models"
        and route != "/models"
    ):
        if global_proxy_spend > litellm.max_budget:
            raise litellm.BudgetExceededError(
                current_cost=global_proxy_spend, max_budget=litellm.max_budget
            )

    _request_metadata: dict = request_body.get("metadata", {}) or {}
    if _request_metadata.get("guardrails"):
        # check if team allowed to modify guardrails
        from litellm.proxy.guardrails.guardrail_helpers import can_modify_guardrails

        can_modify: bool = can_modify_guardrails(team_object)
        if can_modify is False:
            from fastapi import HTTPException

            raise HTTPException(
                status_code=403,
                detail={
                    "error": "Your team does not have permission to modify guardrails."
                },
            )
    is_request_body_safe(request_body=request_body)
    return True

--- a/litellm/proxy/auth/auth_checks.py
+++ b/litellm/proxy/auth/auth_checks.py
@@ -18,6 +18,7 @@
     6. [OPTIONAL] If 'enforce_end_user' enabled - did developer pass in 'user' param for openai endpoints
     7. [OPTIONAL] If 'litellm.max_budget' is set (>0), is proxy under budget
     8. [OPTIONAL] If guardrails modified - is request allowed to change this
+    9. Check if request body is safe
     """
     _model = request_body.get("model", None)
     if team_object is not None and team_object.blocked is True:
@@ -157,4 +158,5 @@
                     "error": "Your team does not have permission to modify guardrails."
                 },
             )
+    is_request_body_safe(request_body=request_body)
     return True

--- /dev/null
+++ b/litellm/proxy/auth/auth_checks.py
@@ -0,0 +1,14 @@
+def is_request_body_safe(request_body: dict) -> bool:
+    """
+    Check if the request body is safe.
+
+    A malicious user can set the \ufeffapi_base to their own domain and invoke POST /chat/completions to intercept and steal the OpenAI API key.
+    Relevant issue: https://huntr.com/bounties/4001e1a2-7b7a-4776-a3ae-e6692ec3d997
+    """
+    banned_params = ["api_base", "base_url"]
+
+    for param in banned_params:
+        if param in request_body:
+            raise ValueError(f"BadRequest: {param} is not allowed in request body")
+
+    return True


open redirect in pollbot (pollbot.services.mozilla.com) in versions before 1.4.6

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.

async def handle_404(request, response):
    if 'json' not in response.headers['Content-Type']:
        if request.path.endswith('/'):
            return web.HTTPFound('/' + request.path.strip('/'))
        return web.json_response({
            "status": 404,
            "message": "Page '{}' not found".format(request.path)
        }, status=404)
    return response

async def handle_404(request, response):
    if 'json' not in response.headers['Content-Type']:
        # This traling slash redirect has caused security issues.
        # If it continues to be problematic, consider:
        #  - only redirect "/v1/.../"?
        #  - remove the redirect entirely; use duplicate routes instead, in app.py
        if request.path.endswith('/'):
            return web.HTTPFound('/' + request.path.strip('/'+string.whitespace))
        return web.json_response({
            "status": 404,
            "message": "Page '{}' not found".format(request.path)
        }, status=404)
    return response

--- a/pollbot/middlewares.py
+++ b/pollbot/middlewares.py
@@ -1,7 +1,11 @@
 async def handle_404(request, response):
     if 'json' not in response.headers['Content-Type']:
+        # This traling slash redirect has caused security issues.
+        # If it continues to be problematic, consider:
+        #  - only redirect "/v1/.../"?
+        #  - remove the redirect entirely; use duplicate routes instead, in app.py
         if request.path.endswith('/'):
-            return web.HTTPFound('/' + request.path.strip('/'))
+            return web.HTTPFound('/' + request.path.strip('/'+string.whitespace))
         return web.json_response({
             "status": 404,
             "message": "Page '{}' not found".format(request.path)


pghoard is a PostgreSQL backup daemon and restore tooling that stores backup data in cloud object stores. A vulnerability has been discovered that could allow an attacker to acquire disk access with privileges equivalent to those of pghoard, allowing for unintended path traversal. Depending on the permissions/privileges assigned to pghoard, this could allow disclosure of sensitive information. This issue has been addressed in releases after 2.2.2a. Users are advised to upgrade. There are no known workarounds for this vulnerability.

    def handle_archival_request(self, site, filename, filetype):
        if filetype == "basebackup":
            # Request a basebackup to be made for site
            self.server.log.debug("Requesting a new basebackup for site: %r to be made", site)
            self.server.requested_basebackup_sites.add(site)
            raise HttpResponse(status=201)

        start_time = time.time()

        site_config = self.server.config["backup_sites"][site]
        xlog_dir = get_pg_wal_directory(site_config)
        xlog_path = os.path.join(xlog_dir, filename)
        self.server.log.debug("Got request to archive: %r %r %r, %r", site, filetype, filename, xlog_path)
        if not os.path.exists(xlog_path):
            self.server.log.debug("xlog_path: %r did not exist, cannot archive, returning 404", xlog_path)
            raise HttpResponse("N/A", status=404)

        if filetype == "xlog":
            try:
                wal.verify_wal(wal_name=filename, filepath=xlog_path)
            except ValueError as ex:
                raise HttpResponse(str(ex), status=412)

        callback_queue = Queue()
        if not self.server.config["backup_sites"][site]["object_storage"]:
            compress_to_memory = False
        else:
            compress_to_memory = True
        if filename.endswith(".history"):
            filetype = FileType.Timeline
        else:
            filetype = FileType.Wal
        compression_event = CompressionEvent(
            callback_queue=callback_queue,
            compress_to_memory=compress_to_memory,
            delete_file_after_compression=False,
            file_path=FileTypePrefixes[filetype] / filename,
            source_data=Path(xlog_path),
            file_type=filetype,
            backup_site_name=site,
            metadata={}
        )
        self.server.compression_queue.put(compression_event)
        try:
            response = callback_queue.get(timeout=30)
            self.server.log.debug(
                "Handled an archival request for: %r %r, took: %.3fs", site, xlog_path,
                time.time() - start_time
            )
        except Empty:
            self.server.log.exception(
                "Problem in getting a response in time, returning 404, took: %.2fs",
                time.time() - start_time
            )
            raise HttpResponse("TIMEOUT", status=500)

        if not response.success:
            raise HttpResponse(status=500)
        raise HttpResponse(status=201)

    def handle_archival_request(self, site, filename, filetype):
        if filetype == "basebackup":
            # Request a basebackup to be made for site
            self.server.log.debug("Requesting a new basebackup for site: %r to be made", site)
            self.server.requested_basebackup_sites.add(site)
            raise HttpResponse(status=201)

        start_time = time.time()

        site_config = self.server.config["backup_sites"][site]
        xlog_dir = get_pg_wal_directory(site_config)
        xlog_path = os.path.join(xlog_dir, filename)
        self.server.log.debug("Got request to archive: %r %r %r, %r", site, filetype, filename, xlog_path)
        if not self._is_valid_xlog_path(xlog_path):
            self.server.log.debug("xlog_path: %r did not exist or contains symlinks, cannot archive, returning 404", xlog_path)
            raise HttpResponse("N/A", status=404)

        if filetype == "xlog":
            try:
                wal.verify_wal(wal_name=filename, filepath=xlog_path)
            except ValueError as ex:
                raise HttpResponse(str(ex), status=412)

        callback_queue = Queue()
        if not self.server.config["backup_sites"][site]["object_storage"]:
            compress_to_memory = False
        else:
            compress_to_memory = True
        if filename.endswith(".history"):
            filetype = FileType.Timeline
        else:
            filetype = FileType.Wal
        compression_event = CompressionEvent(
            callback_queue=callback_queue,
            compress_to_memory=compress_to_memory,
            delete_file_after_compression=False,
            file_path=FileTypePrefixes[filetype] / filename,
            source_data=Path(xlog_path),
            file_type=filetype,
            backup_site_name=site,
            metadata={}
        )
        self.server.compression_queue.put(compression_event)
        try:
            response = callback_queue.get(timeout=30)
            self.server.log.debug(
                "Handled an archival request for: %r %r, took: %.3fs", site, xlog_path,
                time.time() - start_time
            )
        except Empty:
            self.server.log.exception(
                "Problem in getting a response in time, returning 404, took: %.2fs",
                time.time() - start_time
            )
            raise HttpResponse("TIMEOUT", status=500)

        if not response.success:
            raise HttpResponse(status=500)
        raise HttpResponse(status=201)

    @staticmethod
    def _is_valid_xlog_path(xlog_path_str: str) -> bool:
        xlog_path = Path(xlog_path_str)
        return xlog_path.is_file() and not xlog_path.is_symlink()

--- a/pghoard/webserver.py
+++ b/pghoard/webserver.py
@@ -11,8 +11,8 @@
         xlog_dir = get_pg_wal_directory(site_config)
         xlog_path = os.path.join(xlog_dir, filename)
         self.server.log.debug("Got request to archive: %r %r %r, %r", site, filetype, filename, xlog_path)
-        if not os.path.exists(xlog_path):
-            self.server.log.debug("xlog_path: %r did not exist, cannot archive, returning 404", xlog_path)
+        if not self._is_valid_xlog_path(xlog_path):
+            self.server.log.debug("xlog_path: %r did not exist or contains symlinks, cannot archive, returning 404", xlog_path)
             raise HttpResponse("N/A", status=404)

         if filetype == "xlog":

--- /dev/null
+++ b/pghoard/webserver.py
@@ -0,0 +1,4 @@
+    @staticmethod
+    def _is_valid_xlog_path(xlog_path_str: str) -> bool:
+        xlog_path = Path(xlog_path_str)
+        return xlog_path.is_file() and not xlog_path.is_symlink()


Directory traversal vulnerability in minion id validation in SaltStack Salt before 2016.3.8, 2016.11.x before 2016.11.8, and 2017.7.x before 2017.7.2 allows remote minions with incorrect credentials to authenticate to a master via a crafted minion ID.  NOTE: this vulnerability exists because of an incomplete fix for CVE-2017-12791.

def valid_id(opts, id_):
    '''
    Returns if the passed id is valid
    '''
    try:
        return bool(clean_path(opts['pki_dir'], id_)) and clean_id(id_)
    except (AttributeError, KeyError, TypeError) as e:
        return False

def valid_id(opts, id_):
    '''
    Returns if the passed id is valid
    '''
    try:
        if any(x in id_ for x in ('/', '\\', '\0')):
            return False
        return bool(clean_path(opts['pki_dir'], id_))
    except (AttributeError, KeyError, TypeError):
        return False

--- a/salt/utils/verify.py
+++ b/salt/utils/verify.py
@@ -3,6 +3,8 @@
     Returns if the passed id is valid
     '''
     try:
-        return bool(clean_path(opts['pki_dir'], id_)) and clean_id(id_)
-    except (AttributeError, KeyError, TypeError) as e:
+        if any(x in id_ for x in ('/', '\\', '\0')):
+            return False
+        return bool(clean_path(opts['pki_dir'], id_))
+    except (AttributeError, KeyError, TypeError):
         return False