Red Discord Bot before version 3.4.1 has an unauthorized privilege escalation exploit in the Mod module. This exploit allows Discord users with a high privilege level within the guild to bypass hierarchy checks when the application is in a specific condition that is beyond that user's control. By abusing this exploit, it is possible to perform destructive actions within the guild the user has high privileges in. This exploit has been fixed in version 3.4.1. As a workaround, unloading the Mod module with unload mod or, disabling the massban command with command disable global massban can render this exploit not accessible. We still highly recommend updating to 3.4.1 to completely patch this issue.

Improper Authorization

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

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.

Execution with Unnecessary Privileges

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

Improper Privilege Management

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

    async def massban(
        self,
        ctx: commands.Context,
        user_ids: commands.Greedy[RawUserIds],
        days: Optional[int] = None,
        *,
        reason: str = None,
    ):
        """Mass bans user(s) from the server.

        User IDs need to be provided in order to ban
        using this command."""
        banned = []
        errors = {}
        upgrades = []

        async def show_results():
            text = _("Banned {num} users from the server.").format(
                num=humanize_number(len(banned))
            )
            if errors:
                text += _("\nErrors:\n")
                text += "\n".join(errors.values())
            if upgrades:
                text += _(
                    "\nFollowing user IDs have been upgraded from a temporary to a permanent ban:\n"
                )
                text += humanize_list(upgrades)

            for p in pagify(text):
                await ctx.send(p)

        def remove_processed(ids):
            return [_id for _id in ids if _id not in banned and _id not in errors]

        user_ids = list(set(user_ids))  # No dupes

        author = ctx.author
        guild = ctx.guild

        if not user_ids:
            await ctx.send_help()
            return

        if days is None:
            days = await self.config.guild(guild).default_days()

        if not (0 <= days <= 7):
            await ctx.send(_("Invalid days. Must be between 0 and 7."))
            return

        if not guild.me.guild_permissions.ban_members:
            return await ctx.send(_("I lack the permissions to do this."))

        tempbans = await self.config.guild(guild).current_tempbans()

        ban_list = await guild.bans()
        for entry in ban_list:
            for user_id in user_ids:
                if entry.user.id == user_id:
                    if user_id in tempbans:
                        # We need to check if a user is tempbanned here because otherwise they won't be processed later on.
                        continue
                    else:
                        errors[user_id] = _("User with ID {user_id} is already banned.").format(
                            user_id=user_id
                        )

        user_ids = remove_processed(user_ids)

        if not user_ids:
            await show_results()
            return

        for user_id in user_ids:
            user = guild.get_member(user_id)
            if user is not None:
                if user_id in tempbans:
                    # We need to check if a user is tempbanned here because otherwise they won't be processed later on.
                    continue
                else:
                    # Instead of replicating all that handling... gets attr from decorator
                    try:
                        success, reason = await self.ban_user(
                            user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
                        )
                        if success:
                            banned.append(user_id)
                        else:
                            errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
                                user_id=user_id, reason=reason
                            )
                    except Exception as e:
                        errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
                            user_id=user_id, reason=e
                        )

        user_ids = remove_processed(user_ids)

        if not user_ids:
            await show_results()
            return

        for user_id in user_ids:
            user = discord.Object(id=user_id)
            audit_reason = get_audit_reason(author, reason)
            queue_entry = (guild.id, user_id)
            async with self.config.guild(guild).current_tempbans() as tempbans:
                if user_id in tempbans:
                    tempbans.remove(user_id)
                    upgrades.append(str(user_id))
                    log.info(
                        "{}({}) upgraded the tempban for {} to a permaban.".format(
                            author.name, author.id, user_id
                        )
                    )
                    banned.append(user_id)
                else:
                    try:
                        await guild.ban(user, reason=audit_reason, delete_message_days=days)
                        log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id))
                    except discord.NotFound:
                        errors[user_id] = _("User with ID {user_id} not found").format(
                            user_id=user_id
                        )
                        continue
                    except discord.Forbidden:
                        errors[user_id] = _(
                            "Could not ban user with ID {user_id}: missing permissions."
                        ).format(user_id=user_id)
                        continue
                    else:
                        banned.append(user_id)

            await modlog.create_case(
                self.bot,
                guild,
                ctx.message.created_at.replace(tzinfo=timezone.utc),
                "hackban",
                user_id,
                author,
                reason,
                until=None,
                channel=None,
            )
        await show_results()

    async def massban(
        self,
        ctx: commands.Context,
        user_ids: commands.Greedy[RawUserIds],
        days: Optional[int] = None,
        *,
        reason: str = None,
    ):
        """Mass bans user(s) from the server.

        User IDs need to be provided in order to ban
        using this command."""
        banned = []
        errors = {}
        upgrades = []

        async def show_results():
            text = _("Banned {num} users from the server.").format(
                num=humanize_number(len(banned))
            )
            if errors:
                text += _("\nErrors:\n")
                text += "\n".join(errors.values())
            if upgrades:
                text += _(
                    "\nFollowing user IDs have been upgraded from a temporary to a permanent ban:\n"
                )
                text += humanize_list(upgrades)

            for p in pagify(text):
                await ctx.send(p)

        def remove_processed(ids):
            return [_id for _id in ids if _id not in banned and _id not in errors]

        user_ids = list(set(user_ids))  # No dupes

        author = ctx.author
        guild = ctx.guild

        if not user_ids:
            await ctx.send_help()
            return

        if days is None:
            days = await self.config.guild(guild).default_days()

        if not (0 <= days <= 7):
            await ctx.send(_("Invalid days. Must be between 0 and 7."))
            return

        if not guild.me.guild_permissions.ban_members:
            return await ctx.send(_("I lack the permissions to do this."))

        tempbans = await self.config.guild(guild).current_tempbans()

        ban_list = await guild.bans()
        for entry in ban_list:
            for user_id in user_ids:
                if entry.user.id == user_id:
                    if user_id in tempbans:
                        # We need to check if a user is tempbanned here because otherwise they won't be processed later on.
                        continue
                    else:
                        errors[user_id] = _("User with ID {user_id} is already banned.").format(
                            user_id=user_id
                        )

        user_ids = remove_processed(user_ids)

        if not user_ids:
            await show_results()
            return

        # We need to check here, if any of the users isn't a member and if they are,
        # we need to use our `ban_user()` method to do hierarchy checks.
        members: Dict[int, discord.Member] = {}
        to_query: List[int] = []

        for user_id in user_ids:
            member = guild.get_member(user_id)
            if member is not None:
                members[user_id] = member
            elif not guild.chunked:
                to_query.append(user_id)

        # If guild isn't chunked, we might possibly be missing the member from cache,
        # so we need to make sure that isn't the case by querying the user IDs for such guilds.
        while to_query:
            queried_members = await guild.query_members(user_ids=to_query[:100], limit=100)
            members.update((member.id, member) for member in queried_members)
            to_query = to_query[100:]

        # Call `ban_user()` method for all users that turned out to be guild members.
        for member in members:
            try:
                success, reason = await self.ban_user(
                    user=member, ctx=ctx, days=days, reason=reason, create_modlog_case=True
                )
                if success:
                    banned.append(user_id)
                else:
                    errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
                        user_id=user_id, reason=reason
                    )
            except Exception as e:
                errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
                    user_id=user_id, reason=e
                )

        user_ids = remove_processed(user_ids)

        if not user_ids:
            await show_results()
            return

        for user_id in user_ids:
            user = discord.Object(id=user_id)
            audit_reason = get_audit_reason(author, reason)
            queue_entry = (guild.id, user_id)
            async with self.config.guild(guild).current_tempbans() as tempbans:
                if user_id in tempbans:
                    tempbans.remove(user_id)
                    upgrades.append(str(user_id))
                    log.info(
                        "{}({}) upgraded the tempban for {} to a permaban.".format(
                            author.name, author.id, user_id
                        )
                    )
                    banned.append(user_id)
                else:
                    try:
                        await guild.ban(user, reason=audit_reason, delete_message_days=days)
                        log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id))
                    except discord.NotFound:
                        errors[user_id] = _("User with ID {user_id} not found").format(
                            user_id=user_id
                        )
                        continue
                    except discord.Forbidden:
                        errors[user_id] = _(
                            "Could not ban user with ID {user_id}: missing permissions."
                        ).format(user_id=user_id)
                        continue
                    else:
                        banned.append(user_id)

            await modlog.create_case(
                self.bot,
                guild,
                ctx.message.created_at.replace(tzinfo=timezone.utc),
                "hackban",
                user_id,
                author,
                reason,
                until=None,
                channel=None,
            )
        await show_results()

--- a/redbot/cogs/mod/kickban.py
+++ b/redbot/cogs/mod/kickban.py
@@ -72,28 +72,41 @@
             await show_results()
             return

+        # We need to check here, if any of the users isn't a member and if they are,
+        # we need to use our `ban_user()` method to do hierarchy checks.
+        members: Dict[int, discord.Member] = {}
+        to_query: List[int] = []
+
         for user_id in user_ids:
-            user = guild.get_member(user_id)
-            if user is not None:
-                if user_id in tempbans:
-                    # We need to check if a user is tempbanned here because otherwise they won't be processed later on.
-                    continue
+            member = guild.get_member(user_id)
+            if member is not None:
+                members[user_id] = member
+            elif not guild.chunked:
+                to_query.append(user_id)
+
+        # If guild isn't chunked, we might possibly be missing the member from cache,
+        # so we need to make sure that isn't the case by querying the user IDs for such guilds.
+        while to_query:
+            queried_members = await guild.query_members(user_ids=to_query[:100], limit=100)
+            members.update((member.id, member) for member in queried_members)
+            to_query = to_query[100:]
+
+        # Call `ban_user()` method for all users that turned out to be guild members.
+        for member in members:
+            try:
+                success, reason = await self.ban_user(
+                    user=member, ctx=ctx, days=days, reason=reason, create_modlog_case=True
+                )
+                if success:
+                    banned.append(user_id)
                 else:
-                    # Instead of replicating all that handling... gets attr from decorator
-                    try:
-                        success, reason = await self.ban_user(
-                            user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
-                        )
-                        if success:
-                            banned.append(user_id)
-                        else:
-                            errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
-                                user_id=user_id, reason=reason
-                            )
-                    except Exception as e:
-                        errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
-                            user_id=user_id, reason=e
-                        )
+                    errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
+                        user_id=user_id, reason=reason
+                    )
+            except Exception as e:
+                errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
+                    user_id=user_id, reason=e
+                )

         user_ids = remove_processed(user_ids)

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

cd /workspace/Red-DiscordBot
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
/workspace/PoC_env/CVE-2020-15278/bin/python  hand_test.py

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

cd /workspace/Red-DiscordBot
git apply --whitespace=nowarn /workspace/fix.patch
/workspace/PoC_env/CVE-2020-15278/bin/python -m pytest tests/cogs/ -v -k "not test_git_get_full_sha1_from_ambiguous_tag_and_commit and not test_git_get_full_sha1_from_ambiguous_commits"  --asyncio-mode=auto


Open Redirect in GitHub repository archivy/archivy prior to 1.7.0.

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.

@app.before_request
def check_perms():
    allowed_path = (
        request.path.startswith("/login")
        or request.path.startswith("/static")
        or request.path.startswith("/api/login")
    )
    if not current_user.is_authenticated and not allowed_path:
        return redirect(url_for("login", next=request.path))
    return

@app.route("/")
@app.route("/index")
def index():
    path = request.args.get("path", "").lstrip("/")
    try:
        files = data.get_items(path=path)
    except FileNotFoundError:
        flash("Directory does not exist.", "error")
        return redirect("/")

    return render_template(
        "home.html",
        title=path or "root",
        search_enabled=app.config["SEARCH_CONF"]["enabled"],
        dir=files,
        current_path=path,
        new_folder_form=forms.NewFolderForm(),
        delete_form=forms.DeleteFolderForm(),
        rename_form=forms.RenameDirectoryForm(),
        view_only=0,
        search_engine=app.config["SEARCH_CONF"]["engine"],
    )

# TODO: refactor two following methods
@app.route("/bookmarks/new", methods=["GET", "POST"])
def new_bookmark():
    default_dir = app.config.get("DEFAULT_BOOKMARKS_DIR", "root directory")
    form = forms.NewBookmarkForm(path=default_dir)
    form.path.choices = [("", "root directory")] + [
        (pathname, pathname) for pathname in data.get_dirs()
    ]
    if form.validate_on_submit():
        path = form.path.data
        tags = form.tags.data.split(",") if form.tags.data != "" else []
        tags = [tag.strip() for tag in tags]
        bookmark = DataObj(url=form.url.data, tags=tags, path=path, type="bookmark")
        bookmark.process_bookmark_url()
        bookmark_id = bookmark.insert()
        if bookmark_id:
            flash("Bookmark Saved!", "success")
            return redirect(f"/dataobj/{bookmark_id}")
        else:
            flash(bookmark.error, "error")
            return redirect("/bookmarks/new")
    # for bookmarklet
    form.url.data = request.args.get("url", "")
    path = request.args.get("path", default_dir).strip("/")
    # handle empty argument
    form.path.data = path
    return render_template("dataobjs/new.html", title="New Bookmark", form=form)

@app.route("/notes/new", methods=["GET", "POST"])
def new_note():
    form = forms.NewNoteForm()
    default_dir = "root directory"
    form.path.choices = [("", default_dir)] + [
        (pathname, pathname) for pathname in data.get_dirs()
    ]
    if form.validate_on_submit():
        path = form.path.data
        tags = form.tags.data.split(",") if form.tags.data != "" else []
        tags = [tag.strip() for tag in tags]
        note = DataObj(title=form.title.data, path=path, tags=tags, type="note")
        note_id = note.insert()
        if note_id:
            flash("Note Saved!", "success")
            return redirect(f"/dataobj/{note_id}")
    path = request.args.get("path", default_dir).strip("/")
    # handle empty argument
    form.path.data = path
    return render_template("/dataobjs/new.html", title="New Note", form=form)

@app.route("/tags")
def show_all_tags():
    if not app.config["SEARCH_CONF"]["engine"] == "ripgrep" and not which("rg"):
        flash("Ripgrep must be installed to view pages about embedded tags.", "error")
        return redirect("/")
    tags = sorted(get_all_tags(force=True))
    return render_template("tags/all.html", title="All Tags", tags=tags)

@app.route("/tags/<tag_name>")
def show_tag(tag_name):
    if not app.config["SEARCH_CONF"]["enabled"] and not which("rg"):
        flash(
            "Search (for example ripgrep) must be installed to view pages about embedded tags.",
            "error",
        )
        return redirect("/")

    results = search(f"#{tag_name}#", strict=True)
    res_ids = set(
        [item["id"] for item in results]
    )  # avoid duplication of results between context-aware embedded tags and metadata ones
    for res in search_frontmatter_tags(tag_name):
        if res["id"] not in res_ids:
            results.append(res)

    return render_template(
        "tags/show.html",
        title=f"Tags - {tag_name}",
        tag_name=tag_name,
        search_result=results,
    )

@app.route("/dataobj/<int:dataobj_id>")
def show_dataobj(dataobj_id):
    dataobj = data.get_item(dataobj_id)
    get_title_id_pairs = lambda x: (x["title"], x["id"])
    titles = list(
        map(get_title_id_pairs, data.get_items(structured=False, load_content=False))
    )

    if not dataobj:
        flash("Data could not be found!", "error")
        return redirect("/")

    if request.args.get("raw") == "1":
        return frontmatter.dumps(dataobj)

    backlinks = []
    if app.config["SEARCH_CONF"]["enabled"]:
        if app.config["SEARCH_CONF"]["engine"] == "ripgrep":
            query = f"\|{dataobj_id}]]"
        else:
            query = f"|{dataobj_id})]]"
        backlinks = search(query, strict=True)

    # Form for moving data into another folder
    move_form = forms.MoveItemForm()
    move_form.path.choices = [("", "root directory")] + [
        (pathname, pathname) for pathname in data.get_dirs()
    ]

    post_title_form = forms.TitleForm()
    post_title_form.title.data = dataobj["title"]

    # Get all tags
    tag_list = get_all_tags()
    # and the ones present in this dataobj
    embedded_tags = set()
    PATTERN = r"(?:^|\n| )#(?:[-_a-zA-Z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff0-9]+)#"
    for match in re.finditer(PATTERN, dataobj.content):
        embedded_tags.add(match.group(0).replace("#", "").lstrip())

    return render_template(
        "dataobjs/show.html",
        title=dataobj["title"],
        dataobj=dataobj,
        backlinks=backlinks,
        current_path=dataobj["dir"],
        form=forms.DeleteDataForm(),
        view_only=0,
        search_enabled=app.config["SEARCH_CONF"]["enabled"],
        post_title_form=post_title_form,
        move_form=move_form,
        tag_list=tag_list,
        embedded_tags=embedded_tags,
        titles=titles,
    )

@app.route("/dataobj/move/<int:dataobj_id>", methods=["POST"])
def move_item(dataobj_id):
    form = forms.MoveItemForm()
    out_dir = form.path.data if form.path.data != "" else "root directory"
    if form.path.data == None:
        flash("No path specified.")
        return redirect(f"/dataobj/{dataobj_id}")
    try:
        if data.move_item(dataobj_id, form.path.data):
            flash(f"Data successfully moved to {out_dir}.", "success")
            return redirect(f"/dataobj/{dataobj_id}")
        else:
            flash(f"Data could not be moved to {out_dir}.", "error")
            return redirect(f"/dataobj/{dataobj_id}")
    except FileNotFoundError:
        flash("Data not found.", "error")
        return redirect("/")
    except FileExistsError:
        flash("Data already in target directory.", "error")
        return redirect(f"/dataobj/{dataobj_id}")

@app.route("/dataobj/delete/<int:dataobj_id>", methods=["POST"])
def delete_data(dataobj_id):
    try:
        data.delete_item(dataobj_id)
    except BaseException:
        flash("Data could not be found!", "error")
        return redirect("/")
    flash("Data deleted!", "success")
    return redirect("/")

@app.route("/login", methods=["GET", "POST"])
def login():
    form = forms.UserForm()
    if form.validate_on_submit():
        db = get_db()
        user = db.search(
            (Query().username == form.username.data) & (Query().type == "user")
        )

        if user and check_password_hash(user[0]["hashed_password"], form.password.data):
            user = User.from_db(user[0])
            login_user(user, remember=True)
            flash("Login successful!", "success")

            next_url = request.args.get("next")
            return redirect(next_url or "/")

        flash("Invalid credentials", "error")
        return redirect("/login")
    return render_template("users/login.html", form=form, title="Login")

@app.before_request
def check_perms():
    allowed_path = (
        request.path.startswith("/login")
        or request.path.startswith("/static")
        or request.path.startswith("/api/login")
    )
    if not current_user.is_authenticated and not allowed_path:
        return redirect(url_for("login", next=request.path))
    return

@app.route("/")
@app.route("/index")
def index():
    path = request.args.get("path", "").lstrip("/")
    try:
        files = data.get_items(path=path)
    except FileNotFoundError:
        flash("Directory does not exist.", "error")
        return redirect("/")

    return render_template(
        "home.html",
        title=path or "root",
        search_enabled=app.config["SEARCH_CONF"]["enabled"],
        dir=files,
        current_path=path,
        new_folder_form=forms.NewFolderForm(),
        delete_form=forms.DeleteFolderForm(),
        rename_form=forms.RenameDirectoryForm(),
        view_only=0,
        search_engine=app.config["SEARCH_CONF"]["engine"],
    )

# TODO: refactor two following methods
@app.route("/bookmarks/new", methods=["GET", "POST"])
def new_bookmark():
    default_dir = app.config.get("DEFAULT_BOOKMARKS_DIR", "root directory")
    form = forms.NewBookmarkForm(path=default_dir)
    form.path.choices = [("", "root directory")] + [
        (pathname, pathname) for pathname in data.get_dirs()
    ]
    if form.validate_on_submit():
        path = form.path.data
        tags = form.tags.data.split(",") if form.tags.data != "" else []
        tags = [tag.strip() for tag in tags]
        bookmark = DataObj(url=form.url.data, tags=tags, path=path, type="bookmark")
        bookmark.process_bookmark_url()
        bookmark_id = bookmark.insert()
        if bookmark_id:
            flash("Bookmark Saved!", "success")
            return redirect(f"/dataobj/{bookmark_id}")
        else:
            flash(bookmark.error, "error")
            return redirect("/bookmarks/new")
    # for bookmarklet
    form.url.data = request.args.get("url", "")
    path = request.args.get("path", default_dir).strip("/")
    # handle empty argument
    form.path.data = path
    return render_template("dataobjs/new.html", title="New Bookmark", form=form)

@app.route("/notes/new", methods=["GET", "POST"])
def new_note():
    form = forms.NewNoteForm()
    default_dir = "root directory"
    form.path.choices = [("", default_dir)] + [
        (pathname, pathname) for pathname in data.get_dirs()
    ]
    if form.validate_on_submit():
        path = form.path.data
        tags = form.tags.data.split(",") if form.tags.data != "" else []
        tags = [tag.strip() for tag in tags]
        note = DataObj(title=form.title.data, path=path, tags=tags, type="note")
        note_id = note.insert()
        if note_id:
            flash("Note Saved!", "success")
            return redirect(f"/dataobj/{note_id}")
    path = request.args.get("path", default_dir).strip("/")
    # handle empty argument
    form.path.data = path
    return render_template("/dataobjs/new.html", title="New Note", form=form)

@app.route("/tags")
def show_all_tags():
    if not app.config["SEARCH_CONF"]["engine"] == "ripgrep" and not which("rg"):
        flash("Ripgrep must be installed to view pages about embedded tags.", "error")
        return redirect("/")
    tags = sorted(get_all_tags(force=True))
    return render_template("tags/all.html", title="All Tags", tags=tags)

@app.route("/tags/<tag_name>")
def show_tag(tag_name):
    if not app.config["SEARCH_CONF"]["enabled"] and not which("rg"):
        flash(
            "Search (for example ripgrep) must be installed to view pages about embedded tags.",
            "error",
        )
        return redirect("/")

    results = search(f"#{tag_name}#", strict=True)
    res_ids = set(
        [item["id"] for item in results]
    )  # avoid duplication of results between context-aware embedded tags and metadata ones
    for res in search_frontmatter_tags(tag_name):
        if res["id"] not in res_ids:
            results.append(res)

    return render_template(
        "tags/show.html",
        title=f"Tags - {tag_name}",
        tag_name=tag_name,
        search_result=results,
    )

@app.route("/dataobj/<int:dataobj_id>")
def show_dataobj(dataobj_id):
    dataobj = data.get_item(dataobj_id)
    get_title_id_pairs = lambda x: (x["title"], x["id"])
    titles = list(
        map(get_title_id_pairs, data.get_items(structured=False, load_content=False))
    )

    if not dataobj:
        flash("Data could not be found!", "error")
        return redirect("/")

    if request.args.get("raw") == "1":
        return frontmatter.dumps(dataobj)

    backlinks = []
    if app.config["SEARCH_CONF"]["enabled"]:
        if app.config["SEARCH_CONF"]["engine"] == "ripgrep":
            query = f"\|{dataobj_id}]]"
        else:
            query = f"|{dataobj_id})]]"
        backlinks = search(query, strict=True)

    # Form for moving data into another folder
    move_form = forms.MoveItemForm()
    move_form.path.choices = [("", "root directory")] + [
        (pathname, pathname) for pathname in data.get_dirs()
    ]

    post_title_form = forms.TitleForm()
    post_title_form.title.data = dataobj["title"]

    # Get all tags
    tag_list = get_all_tags()
    # and the ones present in this dataobj
    embedded_tags = set()
    PATTERN = r"(?:^|\n| )#(?:[-_a-zA-Z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff0-9]+)#"
    for match in re.finditer(PATTERN, dataobj.content):
        embedded_tags.add(match.group(0).replace("#", "").lstrip())

    return render_template(
        "dataobjs/show.html",
        title=dataobj["title"],
        dataobj=dataobj,
        backlinks=backlinks,
        current_path=dataobj["dir"],
        form=forms.DeleteDataForm(),
        view_only=0,
        search_enabled=app.config["SEARCH_CONF"]["enabled"],
        post_title_form=post_title_form,
        move_form=move_form,
        tag_list=tag_list,
        embedded_tags=embedded_tags,
        titles=titles,
    )

@app.route("/dataobj/move/<int:dataobj_id>", methods=["POST"])
def move_item(dataobj_id):
    form = forms.MoveItemForm()
    out_dir = form.path.data if form.path.data != "" else "root directory"
    if form.path.data == None:
        flash("No path specified.")
        return redirect(f"/dataobj/{dataobj_id}")
    try:
        if data.move_item(dataobj_id, form.path.data):
            flash(f"Data successfully moved to {out_dir}.", "success")
            return redirect(f"/dataobj/{dataobj_id}")
        else:
            flash(f"Data could not be moved to {out_dir}.", "error")
            return redirect(f"/dataobj/{dataobj_id}")
    except FileNotFoundError:
        flash("Data not found.", "error")
        return redirect("/")
    except FileExistsError:
        flash("Data already in target directory.", "error")
        return redirect(f"/dataobj/{dataobj_id}")

@app.route("/dataobj/delete/<int:dataobj_id>", methods=["POST"])
def delete_data(dataobj_id):
    try:
        data.delete_item(dataobj_id)
    except BaseException:
        flash("Data could not be found!", "error")
        return redirect("/")
    flash("Data deleted!", "success")
    return redirect("/")

@app.route("/login", methods=["GET", "POST"])
def login():
    form = forms.UserForm()
    if form.validate_on_submit():
        db = get_db()
        user = db.search(
            (Query().username == form.username.data) & (Query().type == "user")
        )

        if user and check_password_hash(user[0]["hashed_password"], form.password.data):
            user = User.from_db(user[0])
            login_user(user, remember=True)
            flash("Login successful!", "success")

            next_url = request.args.get("next")
            if next_url and is_safe_redirect_url(next_url):
                return redirect(next_url)
            else:
                return redirect("/")

        flash("Invalid credentials", "error")
        return redirect("/login")
    return render_template("users/login.html", form=form, title="Login")

def is_safe_redirect_url(target):
    host_url = urlparse(request.host_url)
    redirect_url = urlparse(urljoin(request.host_url, target))
    return (
        redirect_url.scheme in ("http", "https")
        and host_url.netloc == redirect_url.netloc
    )

--- a/archivy/routes.py
+++ b/archivy/routes.py
@@ -224,7 +224,10 @@
             flash("Login successful!", "success")

             next_url = request.args.get("next")
-            return redirect(next_url or "/")
+            if next_url and is_safe_redirect_url(next_url):
+                return redirect(next_url)
+            else:
+                return redirect("/")

         flash("Invalid credentials", "error")
         return redirect("/login")

--- /dev/null
+++ b/archivy/routes.py
@@ -0,0 +1,7 @@
+def is_safe_redirect_url(target):
+    host_url = urlparse(request.host_url)
+    redirect_url = urlparse(urljoin(request.host_url, target))
+    return (
+        redirect_url.scheme in ("http", "https")
+        and host_url.netloc == redirect_url.netloc
+    )


In mintplex-labs/anything-llm, a vulnerability exists in the thread update process that allows users with Default or Manager roles to escalate their privileges to Administrator. The issue arises from improper input validation when handling HTTP POST requests to the endpoint `/workspace/:slug/thread/:threadSlug/update`. Specifically, the application fails to validate or check user input before passing it to the `workspace_thread` Prisma model for execution. This oversight allows attackers to craft a Prisma relation query operation that manipulates the `users` model to change a user's role to admin. Successful exploitation grants attackers the highest level of user privileges, enabling them to see and perform all actions within the system.

Improper Handling of Exceptional Conditions

The product does not handle or incorrectly handles an exceptional condition.

  update: async function (prevThread = null, data = {}) {
    if (!prevThread) throw new Error("No thread id provided for update");

    const validKeys = Object.keys(data).filter((key) =>
      this.writable.includes(key)
    );
    if (validKeys.length === 0)
      return { thread: prevThread, message: "No valid fields to update!" };

    try {
      const thread = await prisma.workspace_threads.update({
        where: { id: prevThread.id },
        data,
      });
      return { thread, message: null };
    } catch (error) {
      console.error(error.message);
      return { thread: null, message: error.message };
    }
  },

  update: async function (prevThread = null, data = {}) {
    if (!prevThread) throw new Error("No thread id provided for update");

    const validData = {};
    Object.entries(data).forEach(([key, value]) => {
      if (!this.writable.includes(key)) return;
      validData[key] = value;
    });

    if (Object.keys(validData).length === 0)
      return { thread: prevThread, message: "No valid fields to update!" };

    try {
      const thread = await prisma.workspace_threads.update({
        where: { id: prevThread.id },
        data: validData,
      });
      return { thread, message: null };
    } catch (error) {
      console.error(error.message);
      return { thread: null, message: error.message };
    }
  },

--- a/server/models/workspaceThread.js
+++ b/server/models/workspaceThread.js
@@ -1,16 +1,19 @@
   update: async function (prevThread = null, data = {}) {
     if (!prevThread) throw new Error("No thread id provided for update");

-    const validKeys = Object.keys(data).filter((key) =>
-      this.writable.includes(key)
-    );
-    if (validKeys.length === 0)
+    const validData = {};
+    Object.entries(data).forEach(([key, value]) => {
+      if (!this.writable.includes(key)) return;
+      validData[key] = value;
+    });
+
+    if (Object.keys(validData).length === 0)
       return { thread: prevThread, message: "No valid fields to update!" };

     try {
       const thread = await prisma.workspace_threads.update({
         where: { id: prevThread.id },
-        data,
+        data: validData,
       });
       return { thread, message: null };
     } catch (error) {


A code injection exists in node-df v0.1.4 that can allow an attacker to remote code execution by unsanitized input.

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.

    exec(command, function(err, stdout, stderr) {
        if (err) {
            callback(err)
            return
        }

        if (stderr) {
            callback(new Error(err))
            return
        }

        var entries = parse(stdout, options)

        callback(null, entries)
    })

// escaping chars to prevent command injection
    var escapeShell = function(command) {
        return '"'+command.replace(/(["\s'$`\])/g,'\$1')+'"'
    };
    exec(escapeShell(command), function(err, stdout, stderr) {
        if (err) {
            callback(err)
            return
        }

        if (stderr) {
            callback(new Error(err))
            return
        }

        var entries = parse(stdout, options)

        callback(null, entries)
    })

--- a/lib/index.js
+++ b/lib/index.js
@@ -1,4 +1,4 @@
-    exec(command, function(err, stdout, stderr) {
+    exec(escapeShell(command), function(err, stdout, stderr) {
         if (err) {
             callback(err)
             return

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

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

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

cd /workspace/node-df
git apply --whitespace=nowarn /workspace/fix.patch
npm test lib/__tests__/calcMultipliers.spec.js lib/__tests__/index.spec.js


A vulnerability was found in 2071174A vinylmap. It has been classified as critical. Affected is the function contact of the file recordstoreapp/views.py. The manipulation leads to sql injection. The name of the patch is b07b79a1e92cc62574ba0492cce000ef4a7bd25f. It is recommended to apply a patch to fix this issue. The identifier of this vulnerability is VDB-218400.

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 search(request):
	context_dict = {}
	if 'q' in request.GET and request.GET['q'] != '':
		q = request.GET['q']
		cursor = connection.cursor()
		cursor.execute("SELECT id,title,artist,cover FROM recordstoreapp_record WHERE title like '%" + q + "%' or artist like '%" + q + "%' or label like '%" + q + "%' or cat_no like '%" + q + "%';")
		rec_list=cursor.fetchall()

		total=len(rec_list)
		pg=int(request.GET['page']) if 'page' in request.GET else 1
		ub=min(pg*12, total)

		context_dict['rec_list'] = rec_list[(pg-1)*12:ub]
		maxrange = int(total/12)
		if total%12 > 0:
			maxrange = maxrange + 1
		if maxrange == 1:
			maxrange = 0
		context_dict['range'] = range(1,maxrange+1)
		print total
		context_dict['q'] = q

	return render(request, 'search.html', context_dict)

def search(request):
	context_dict = {}
	q = request.GET['q'].replace('%', '').replace('_', '').strip()
	if 'q' in request.GET and q != '':
		q = '%' + q + '%'
		cursor = connection.cursor()
		cursor.execute("SELECT id,title,artist,cover FROM recordstoreapp_record WHERE title like %s or artist like %s or label like %s or cat_no like %s;", [q,q,q,q])
		rec_list=cursor.fetchall()

		total=len(rec_list)
		pg=int(request.GET['page']) if 'page' in request.GET else 1
		ub=min(pg*12, total)

		context_dict['rec_list'] = rec_list[(pg-1)*12:ub]
		maxrange = int(total/12)
		if total%12 > 0:
			maxrange = maxrange + 1
		if maxrange == 1:
			maxrange = 0
		context_dict['range'] = range(1,maxrange+1)
		print total
		context_dict['q'] = q

	return render(request, 'search.html', context_dict)

--- a/recordstoreapp/views.py
+++ b/recordstoreapp/views.py
@@ -1,11 +1,13 @@
 def search(request):
 	context_dict = {}
-	if 'q' in request.GET and request.GET['q'] != '':
-		q = request.GET['q']
+	q = request.GET['q'].replace('%', '').replace('_', '').strip()
+	if 'q' in request.GET and q != '':
+		q = '%' + q + '%'
 		cursor = connection.cursor()
-		cursor.execute("SELECT id,title,artist,cover FROM recordstoreapp_record WHERE title like '%" + q + "%' or artist like '%" + q + "%' or label like '%" + q + "%' or cat_no like '%" + q + "%';")
+		cursor.execute("SELECT id,title,artist,cover FROM recordstoreapp_record WHERE title like %s or artist like %s or label like %s or cat_no like %s;", [q,q,q,q])
 		rec_list=cursor.fetchall()

+
 		total=len(rec_list)
 		pg=int(request.GET['page']) if 'page' in request.GET else 1
 		ub=min(pg*12, total)


GeoNode is an open source platform that facilitates the creation, sharing, and collaborative use of geospatial data. A SSRF vulnerability exists starting in version 3.2.0, bypassing existing controls on the software. This can allow a user to request internal services for a full read SSRF, returning any data from the internal network. The application is using a whitelist, but the whitelist can be bypassed. The bypass will trick the application that the first host is a whitelisted address, but the browser will use `@` or `%40` as a credential to the host geoserver on port 8080, this will return the data to that host on the response. Version 4.1.3.post1 is the first available version that contains a patch.

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 extract_ip_or_domain(url):
    ip_regex = re.compile("^(?:http://|https://)(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})")
    domain_regex = re.compile("^(?:http://|https://)([a-zA-Z0-9.-]+)")

    match = ip_regex.findall(url)
    if len(match):
        ip_address = match[0]
        try:
            ipaddress.ip_address(ip_address)  # Validate the IP address
            return ip_address
        except ValueError:
            pass

    match = domain_regex.findall(url)
    if len(match):
        return match[0]

    return None

def remove_credentials_from_url(url):
    # Parse the URL
    parsed_url = urlparse(url)

    # Remove the username and password from the parsed URL
    parsed_url = parsed_url._replace(netloc=parsed_url.netloc.split("@")[-1])

    # Reconstruct the URL without credentials
    cleaned_url = urlunparse(parsed_url)

    return cleaned_url

def extract_ip_or_domain(url):
    # Decode the URL to handle percent-encoded characters
    _url = remove_credentials_from_url(unquote(url))

    ip_regex = re.compile("^(?:http://|https://)(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})")
    domain_regex = re.compile("^(?:http://|https://)([a-zA-Z0-9.-]+)")

    match = ip_regex.findall(_url)
    if len(match):
        ip_address = match[0]
        try:
            ipaddress.ip_address(ip_address)  # Validate the IP address
            return ip_address
        except ValueError:
            pass

    match = domain_regex.findall(_url)
    if len(match):
        return match[0]

    return None

--- a/geonode/utils.py
+++ b/geonode/utils.py
@@ -1,8 +1,11 @@
 def extract_ip_or_domain(url):
+    # Decode the URL to handle percent-encoded characters
+    _url = remove_credentials_from_url(unquote(url))
+
     ip_regex = re.compile("^(?:http://|https://)(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})")
     domain_regex = re.compile("^(?:http://|https://)([a-zA-Z0-9.-]+)")

-    match = ip_regex.findall(url)
+    match = ip_regex.findall(_url)
     if len(match):
         ip_address = match[0]
         try:
@@ -11,7 +14,7 @@
         except ValueError:
             pass

-    match = domain_regex.findall(url)
+    match = domain_regex.findall(_url)
     if len(match):
         return match[0]

--- /dev/null
+++ b/geonode/utils.py
@@ -0,0 +1,11 @@
+def remove_credentials_from_url(url):
+    # Parse the URL
+    parsed_url = urlparse(url)
+
+    # Remove the username and password from the parsed URL
+    parsed_url = parsed_url._replace(netloc=parsed_url.netloc.split("@")[-1])
+
+    # Reconstruct the URL without credentials
+    cleaned_url = urlunparse(parsed_url)
+
+    return cleaned_url


Growl adds growl notification support to nodejs. Growl before 1.10.2 does not properly sanitize input before passing it to exec, allowing for arbitrary command execution.

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.

function growl(msg, options, fn) {
  var image
    , args
    , options = options || {}
    , fn = fn || function(){};

  if (options.exec) {
    cmd = {
        type: "Custom"
      , pkg: options.exec
      , range: []
    };
  }

  // noop
  if (!cmd) return fn(new Error('growl not supported on this platform'));
  args = [cmd.pkg];

  // image
  if (image = options.image) {
    switch(cmd.type) {
      case 'Darwin-Growl':
        var flag, ext = path.extname(image).substr(1)
        flag = flag || ext == 'icns' && 'iconpath'
        flag = flag || /^[A-Z]/.test(image) && 'appIcon'
        flag = flag || /^png|gif|jpe?g$/.test(ext) && 'image'
        flag = flag || ext && (image = ext) && 'icon'
        flag = flag || 'icon'
        args.push('--' + flag, quote(image))
        break;
      case 'Darwin-NotificationCenter':
        args.push(cmd.icon, quote(image));
        break;
      case 'Linux':
        args.push(cmd.icon, quote(image));
        // libnotify defaults to sticky, set a hint for transient notifications
        if (!options.sticky) args.push('--hint=int:transient:1');
        break;
      case 'Windows':
        args.push(cmd.icon + quote(image));
        break;
    }
  }

  // sticky
  if (options.sticky) args.push(cmd.sticky);

  // priority
  if (options.priority) {
    var priority = options.priority + '';
    var checkindexOf = cmd.priority.range.indexOf(priority);
    if (~cmd.priority.range.indexOf(priority)) {
      args.push(cmd.priority, options.priority);
    }
  }

  //sound
  if(options.sound && cmd.type === 'Darwin-NotificationCenter'){
    args.push(cmd.sound, options.sound)
  }

  // name
  if (options.name && cmd.type === "Darwin-Growl") {
    args.push('--name', options.name);
  }

  switch(cmd.type) {
    case 'Darwin-Growl':
      args.push(cmd.msg);
      args.push(quote(msg).replace(/\\n/g, '\n'));
      if (options.title) args.push(quote(options.title));
      break;
    case 'Darwin-NotificationCenter':
      args.push(cmd.msg);
      var stringifiedMsg = quote(msg);
      var escapedMsg = stringifiedMsg.replace(/\\n/g, '\n');
      args.push(escapedMsg);
      if (options.title) {
        args.push(cmd.title);
        args.push(quote(options.title));
      }
      if (options.subtitle) {
        args.push(cmd.subtitle);
        args.push(quote(options.subtitle));
      }
      if (options.url) {
        args.push(cmd.url);
        args.push(quote(options.url));
      }
      break;
    case 'Linux-Growl':
      args.push(cmd.msg);
      args.push(quote(msg).replace(/\\n/g, '\n'));
      if (options.title) args.push(quote(options.title));
      if (cmd.host) {
        args.push(cmd.host.cmd, cmd.host.hostname)
      }
      break;
    case 'Linux':
      if (options.title) {
        args.push(quote(options.title));
        args.push(cmd.msg);
        args.push(quote(msg).replace(/\\n/g, '\n'));
      } else {
        args.push(quote(msg).replace(/\\n/g, '\n'));
      }
      break;
    case 'Windows':
      args.push(quote(msg).replace(/\\n/g, '\n'));
      if (options.title) args.push(cmd.title + quote(options.title));
      if (options.url) args.push(cmd.url + quote(options.url));
      break;
    case 'Custom':
      args[0] = (function(origCommand) {
        var message = options.title
          ? options.title + ': ' + msg
          : msg;
        var command = origCommand.replace(/(^|[^%])%s/g, '$1' + quote(message));
        if (command === origCommand) args.push(quote(message));
        return command;
      })(args[0]);
      break;
  }

  // execute
  exec(args.join(' '), fn);
};

function growl(msg, options, fn) {
  var image
    , args
    , options = options || {}
    , fn = fn || function(){};

  if (options.exec) {
    cmd = {
        type: "Custom"
      , pkg: options.exec
      , range: []
    };
  }

  // noop
  if (!cmd) return fn(new Error('growl not supported on this platform'));
  args = [cmd.pkg];

  // image
  if (image = options.image) {
    switch(cmd.type) {
      case 'Darwin-Growl':
        var flag, ext = path.extname(image).substr(1)
        flag = flag || ext == 'icns' && 'iconpath'
        flag = flag || /^[A-Z]/.test(image) && 'appIcon'
        flag = flag || /^png|gif|jpe?g$/.test(ext) && 'image'
        flag = flag || ext && (image = ext) && 'icon'
        flag = flag || 'icon'
        args.push('--' + flag, image)
        break;
      case 'Darwin-NotificationCenter':
        args.push(cmd.icon, image);
        break;
      case 'Linux':
        args.push(cmd.icon, image);
        // libnotify defaults to sticky, set a hint for transient notifications
        if (!options.sticky) args.push('--hint=int:transient:1');
        break;
      case 'Windows':
        args.push(cmd.icon + image);
        break;
    }
  }

  // sticky
  if (options.sticky) args.push(cmd.sticky);

  // priority
  if (options.priority) {
    var priority = options.priority + '';
    var checkindexOf = cmd.priority.range.indexOf(priority);
    if (~cmd.priority.range.indexOf(priority)) {
      args.push(cmd.priority, options.priority);
    }
  }

  //sound
  if(options.sound && cmd.type === 'Darwin-NotificationCenter'){
    args.push(cmd.sound, options.sound)
  }

  // name
  if (options.name && cmd.type === "Darwin-Growl") {
    args.push('--name', options.name);
  }

  switch(cmd.type) {
    case 'Darwin-Growl':
      args.push(cmd.msg);
      args.push(msg.replace(/\\n/g, '\n'));
      if (options.title) args.push(options.title);
      break;
    case 'Darwin-NotificationCenter':
      args.push(cmd.msg);
      var stringifiedMsg = msg;
      var escapedMsg = stringifiedMsg.replace(/\\n/g, '\n');
      args.push(escapedMsg);
      if (options.title) {
        args.push(cmd.title);
        args.push(options.title);
      }
      if (options.subtitle) {
        args.push(cmd.subtitle);
        args.push(options.subtitle);
      }
      if (options.url) {
        args.push(cmd.url);
        args.push(options.url);
      }
      break;
    case 'Linux-Growl':
      args.push(cmd.msg);
      args.push(msg.replace(/\\n/g, '\n'));
      if (options.title) args.push(options.title);
      if (cmd.host) {
        args.push(cmd.host.cmd, cmd.host.hostname)
      }
      break;
    case 'Linux':
      if (options.title) {
        args.push(options.title);
        args.push(cmd.msg);
        args.push(msg.replace(/\\n/g, '\n'));
      } else {
        args.push(msg.replace(/\\n/g, '\n'));
      }
      break;
    case 'Windows':
      args.push(msg.replace(/\\n/g, '\n'));
      if (options.title) args.push(cmd.title + options.title);
      if (options.url) args.push(cmd.url + options.url);
      break;
    case 'Custom':
      args[0] = (function(origCommand) {
        var message = options.title
          ? options.title + ': ' + msg
          : msg;
        var command = origCommand.replace(/(^|[^%])%s/g, '$1' + message);
        if (command === origCommand) args.push(message);
        return command;
      })(args[0]);
      break;
  }
  var cmd_to_exec = args[0];
  args.shift();
  spawn(cmd_to_exec, args);
};

--- a/lib/growl.js
+++ b/lib/growl.js
@@ -26,18 +26,18 @@
         flag = flag || /^png|gif|jpe?g$/.test(ext) && 'image'
         flag = flag || ext && (image = ext) && 'icon'
         flag = flag || 'icon'
-        args.push('--' + flag, quote(image))
+        args.push('--' + flag, image)
         break;
       case 'Darwin-NotificationCenter':
-        args.push(cmd.icon, quote(image));
+        args.push(cmd.icon, image);
         break;
       case 'Linux':
-        args.push(cmd.icon, quote(image));
+        args.push(cmd.icon, image);
         // libnotify defaults to sticky, set a hint for transient notifications
         if (!options.sticky) args.push('--hint=int:transient:1');
         break;
       case 'Windows':
-        args.push(cmd.icon + quote(image));
+        args.push(cmd.icon + image);
         break;
     }
   }
@@ -67,61 +67,61 @@
   switch(cmd.type) {
     case 'Darwin-Growl':
       args.push(cmd.msg);
-      args.push(quote(msg).replace(/\\n/g, '\n'));
-      if (options.title) args.push(quote(options.title));
+      args.push(msg.replace(/\\n/g, '\n'));
+      if (options.title) args.push(options.title);
       break;
     case 'Darwin-NotificationCenter':
       args.push(cmd.msg);
-      var stringifiedMsg = quote(msg);
+      var stringifiedMsg = msg;
       var escapedMsg = stringifiedMsg.replace(/\\n/g, '\n');
       args.push(escapedMsg);
       if (options.title) {
         args.push(cmd.title);
-        args.push(quote(options.title));
+        args.push(options.title);
       }
       if (options.subtitle) {
         args.push(cmd.subtitle);
-        args.push(quote(options.subtitle));
+        args.push(options.subtitle);
       }
       if (options.url) {
         args.push(cmd.url);
-        args.push(quote(options.url));
+        args.push(options.url);
       }
       break;
     case 'Linux-Growl':
       args.push(cmd.msg);
-      args.push(quote(msg).replace(/\\n/g, '\n'));
-      if (options.title) args.push(quote(options.title));
+      args.push(msg.replace(/\\n/g, '\n'));
+      if (options.title) args.push(options.title);
       if (cmd.host) {
         args.push(cmd.host.cmd, cmd.host.hostname)
       }
       break;
     case 'Linux':
       if (options.title) {
-        args.push(quote(options.title));
+        args.push(options.title);
         args.push(cmd.msg);
-        args.push(quote(msg).replace(/\\n/g, '\n'));
+        args.push(msg.replace(/\\n/g, '\n'));
       } else {
-        args.push(quote(msg).replace(/\\n/g, '\n'));
+        args.push(msg.replace(/\\n/g, '\n'));
       }
       break;
     case 'Windows':
-      args.push(quote(msg).replace(/\\n/g, '\n'));
-      if (options.title) args.push(cmd.title + quote(options.title));
-      if (options.url) args.push(cmd.url + quote(options.url));
+      args.push(msg.replace(/\\n/g, '\n'));
+      if (options.title) args.push(cmd.title + options.title);
+      if (options.url) args.push(cmd.url + options.url);
       break;
     case 'Custom':
       args[0] = (function(origCommand) {
         var message = options.title
           ? options.title + ': ' + msg
           : msg;
-        var command = origCommand.replace(/(^|[^%])%s/g, '$1' + quote(message));
-        if (command === origCommand) args.push(quote(message));
+        var command = origCommand.replace(/(^|[^%])%s/g, '$1' + message);
+        if (command === origCommand) args.push(message);
         return command;
       })(args[0]);
       break;
   }
-
-  // execute
-  exec(args.join(' '), fn);
+  var cmd_to_exec = args[0];
+  args.shift();
+  spawn(cmd_to_exec, args);
 };

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

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


PyAnsys Geometry is a Python client library for the Ansys Geometry service and other CAD Ansys products. On file src/ansys/geometry/core/connection/product_instance.py, upon calling this method _start_program directly, users could exploit its usage to perform malicious operations on the current machine where the script is ran. This vulnerability is fixed in 0.3.3 and 0.4.12.

def prepare_and_start_backend(
    backend_type: BackendType,
    product_version: int = None,
    host: str = "localhost",
    port: int = None,
    enable_trace: bool = False,
    log_level: int = 2,
    api_version: ApiVersions = ApiVersions.LATEST,
    timeout: int = 150,
    manifest_path: str = None,
    logs_folder: str = None,
    hidden: bool = False,
) -> "Modeler":
    """
    Start the requested service locally using the ``ProductInstance`` class.

    When calling this method, a standalone service or product session is started.
    By default, if an endpoint is specified (by defining `host` and `port` parameters)
    but the endpoint is not available, the startup will fail. Otherwise, it will try to
    launch its own service.

    Parameters
    ----------
    product_version: ``int``, optional
        The product version to be started. Goes from v23.2.1 to
        the latest. Default is ``None``.
        If a specific product version is requested but not installed locally,
        a SystemError will be raised.
    host: str, optional
        IP address at which the Geometry service will be deployed. By default,
        its value will be ``localhost``.
    port : int, optional
        Port at which the Geometry service will be deployed. By default, its
        value will be ``None``.
    enable_trace : bool, optional
        Boolean enabling the logs trace on the Geometry service console window.
        By default its value is ``False``.
    log_level : int, optional
        Backend's log level from 0 to 3:
            0: Chatterbox
            1: Debug
            2: Warning
            3: Error

        The default is ``2`` (Warning).
    api_version: ``ApiVersions``, optional
        The backend's API version to be used at runtime. Goes from API v21 to
        the latest. Default is ``ApiVersions.LATEST``.
    timeout : int, optional
        Timeout for starting the backend startup process. The default is 150.
    manifest_path : str, optional
        Used to specify a manifest file path for the ApiServerAddin. This way,
        it is possible to run an ApiServerAddin from a version an older product
        version. Only applicable for Ansys Discovery and Ansys SpaceClaim.
    logs_folder : sets the backend's logs folder path. If nothing is defined,
        the backend will use its default path.
    hidden : starts the product hiding its UI. Default is ``False``.

    Raises
    ------
    ConnectionError
        If the specified endpoint is already in use, a connection error will be raised.
    SystemError
        If there is not an Ansys product 23.2 version or later installed
        or if a specific product's version is requested but not installed locally then
        a SystemError will be raised.

    Returns
    -------
    Modeler
        Instance of the Geometry service.
    """
    from ansys.geometry.core.modeler import Modeler

    port = _check_port_or_get_one(port)
    installations = get_available_ansys_installations()
    if product_version != None:
        _check_version_is_available(product_version, installations)
    else:
        product_version = get_latest_ansys_installation()[0]
        _check_minimal_versions(product_version)

    args = []
    env_copy = _get_common_env(
        host=host,
        port=port,
        enable_trace=enable_trace,
        log_level=log_level,
        logs_folder=logs_folder,
    )

    if backend_type == BackendType.DISCOVERY:
        args.append(os.path.join(installations[product_version], DISCOVERY_FOLDER, DISCOVERY_EXE))
        if hidden is True:
            args.append(BACKEND_DISCOVERY_HIDDEN)

        # Here begins the spaceclaim arguments.
        args.append(BACKEND_SPACECLAIM_OPTIONS)
        args.append(
            BACKEND_ADDIN_MANIFEST_ARGUMENT
            + _manifest_path_provider(product_version, installations, manifest_path)
        )
        env_copy[BACKEND_API_VERSION_VARIABLE] = str(api_version)

    elif backend_type == BackendType.SPACECLAIM:
        args.append(os.path.join(installations[product_version], SPACECLAIM_FOLDER, SPACECLAIM_EXE))
        if hidden is True:
            args.append(BACKEND_SPACECLAIM_HIDDEN)
            args.append(BACKEND_SPLASH_OFF)
        args.append(
            BACKEND_ADDIN_MANIFEST_ARGUMENT
            + _manifest_path_provider(product_version, installations, manifest_path)
        )
        env_copy[BACKEND_API_VERSION_VARIABLE] = str(api_version)
        env_copy[BACKEND_SPACECLAIM_HIDDEN_ENVVAR_KEY] = BACKEND_SPACECLAIM_HIDDEN_ENVVAR_VALUE

    elif backend_type == BackendType.WINDOWS_SERVICE:
        latest_version = get_latest_ansys_installation()[0]
        args.append(
            os.path.join(
                installations[latest_version], WINDOWS_GEOMETRY_SERVICE_FOLDER, GEOMETRY_SERVICE_EXE
            )
        )
    else:
        raise RuntimeError(
            f"Cannot connect to backend {backend_type.name} using ``prepare_and_start_backend()``"
        )

    LOG.info(f"Launching ProductInstance for {backend_type.name}")
    LOG.debug(f"Args: {args}")
    LOG.debug(f"Environment variables: {env_copy}")

    instance = ProductInstance(_start_program(args, env_copy).pid)

    # Verify that the backend is ready to accept connections
    # before returning the Modeler instance.
    LOG.info("Waiting for backend to be ready...")
    _wait_for_backend(host, port, timeout)

    return Modeler(
        host=host, port=port, timeout=timeout, product_instance=instance, backend_type=backend_type
    )

def _start_program(args: List[str], local_env: Dict[str, str]) -> subprocess.Popen:
    """
    Start the program where the path is the first item of the ``args`` array argument.

    Parameters
    ----------
    args : List[str]
        List of arguments to be passed to the program. The first list's item shall
        be the program path.
    local_env : Dict[str,str]
        Environment variables to be passed to the program.

    Returns
    -------
    subprocess.Popen
        The subprocess object.
    """
    return subprocess.Popen(
        args,
        shell=os.name != "nt",
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        env=local_env,
    )

def prepare_and_start_backend(
    backend_type: BackendType,
    product_version: int = None,
    host: str = "localhost",
    port: int = None,
    enable_trace: bool = False,
    log_level: int = 2,
    api_version: ApiVersions = ApiVersions.LATEST,
    timeout: int = 150,
    manifest_path: str = None,
    logs_folder: str = None,
    hidden: bool = False,
) -> "Modeler":
    """
    Start the requested service locally using the ``ProductInstance`` class.

    When calling this method, a standalone service or product session is started.
    By default, if an endpoint is specified (by defining `host` and `port` parameters)
    but the endpoint is not available, the startup will fail. Otherwise, it will try to
    launch its own service.

    Parameters
    ----------
    product_version: ``int``, optional
        The product version to be started. Goes from v23.2.1 to
        the latest. Default is ``None``.
        If a specific product version is requested but not installed locally,
        a SystemError will be raised.
    host: str, optional
        IP address at which the Geometry service will be deployed. By default,
        its value will be ``localhost``.
    port : int, optional
        Port at which the Geometry service will be deployed. By default, its
        value will be ``None``.
    enable_trace : bool, optional
        Boolean enabling the logs trace on the Geometry service console window.
        By default its value is ``False``.
    log_level : int, optional
        Backend's log level from 0 to 3:
            0: Chatterbox
            1: Debug
            2: Warning
            3: Error

        The default is ``2`` (Warning).
    api_version: ``ApiVersions``, optional
        The backend's API version to be used at runtime. Goes from API v21 to
        the latest. Default is ``ApiVersions.LATEST``.
    timeout : int, optional
        Timeout for starting the backend startup process. The default is 150.
    manifest_path : str, optional
        Used to specify a manifest file path for the ApiServerAddin. This way,
        it is possible to run an ApiServerAddin from a version an older product
        version. Only applicable for Ansys Discovery and Ansys SpaceClaim.
    logs_folder : sets the backend's logs folder path. If nothing is defined,
        the backend will use its default path.
    hidden : starts the product hiding its UI. Default is ``False``.

    Raises
    ------
    ConnectionError
        If the specified endpoint is already in use, a connection error will be raised.
    SystemError
        If there is not an Ansys product 23.2 version or later installed
        or if a specific product's version is requested but not installed locally then
        a SystemError will be raised.

    Returns
    -------
    Modeler
        Instance of the Geometry service.
    """
    from ansys.geometry.core.modeler import Modeler

    if os.name != "nt":  # pragma: no cover
        raise RuntimeError("Method 'prepare_and_start_backend' is only available on Windows.")

    port = _check_port_or_get_one(port)
    installations = get_available_ansys_installations()
    if product_version != None:
        _check_version_is_available(product_version, installations)
    else:
        product_version = get_latest_ansys_installation()[0]
        _check_minimal_versions(product_version)

    args = []
    env_copy = _get_common_env(
        host=host,
        port=port,
        enable_trace=enable_trace,
        log_level=log_level,
        logs_folder=logs_folder,
    )

    if backend_type == BackendType.DISCOVERY:
        args.append(os.path.join(installations[product_version], DISCOVERY_FOLDER, DISCOVERY_EXE))
        if hidden is True:
            args.append(BACKEND_DISCOVERY_HIDDEN)

        # Here begins the spaceclaim arguments.
        args.append(BACKEND_SPACECLAIM_OPTIONS)
        args.append(
            BACKEND_ADDIN_MANIFEST_ARGUMENT
            + _manifest_path_provider(product_version, installations, manifest_path)
        )
        env_copy[BACKEND_API_VERSION_VARIABLE] = str(api_version)

    elif backend_type == BackendType.SPACECLAIM:
        args.append(os.path.join(installations[product_version], SPACECLAIM_FOLDER, SPACECLAIM_EXE))
        if hidden is True:
            args.append(BACKEND_SPACECLAIM_HIDDEN)
            args.append(BACKEND_SPLASH_OFF)
        args.append(
            BACKEND_ADDIN_MANIFEST_ARGUMENT
            + _manifest_path_provider(product_version, installations, manifest_path)
        )
        env_copy[BACKEND_API_VERSION_VARIABLE] = str(api_version)
        env_copy[BACKEND_SPACECLAIM_HIDDEN_ENVVAR_KEY] = BACKEND_SPACECLAIM_HIDDEN_ENVVAR_VALUE

    elif backend_type == BackendType.WINDOWS_SERVICE:
        latest_version = get_latest_ansys_installation()[0]
        args.append(
            os.path.join(
                installations[latest_version], WINDOWS_GEOMETRY_SERVICE_FOLDER, GEOMETRY_SERVICE_EXE
            )
        )
    else:
        raise RuntimeError(
            f"Cannot connect to backend {backend_type.name} using ``prepare_and_start_backend()``"
        )

    LOG.info(f"Launching ProductInstance for {backend_type.name}")
    LOG.debug(f"Args: {args}")
    LOG.debug(f"Environment variables: {env_copy}")

    instance = ProductInstance(_start_program(args, env_copy).pid)

    # Verify that the backend is ready to accept connections
    # before returning the Modeler instance.
    LOG.info("Waiting for backend to be ready...")
    _wait_for_backend(host, port, timeout)

    return Modeler(
        host=host, port=port, timeout=timeout, product_instance=instance, backend_type=backend_type
    )

def _start_program(args: List[str], local_env: Dict[str, str]) -> subprocess.Popen:
    """
    Start the program where the path is the first item of the ``args`` array argument.

    Parameters
    ----------
    args : List[str]
        List of arguments to be passed to the program. The first list's item shall
        be the program path.
    local_env : Dict[str,str]
        Environment variables to be passed to the program.

    Returns
    -------
    subprocess.Popen
        The subprocess object.
    """
    return subprocess.Popen(
        args,
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        env=local_env,
    )

--- a/src/ansys/geometry/core/connection/product_instance.py
+++ b/src/ansys/geometry/core/connection/product_instance.py
@@ -72,6 +72,9 @@
     """
     from ansys.geometry.core.modeler import Modeler

+    if os.name != "nt":  # pragma: no cover
+        raise RuntimeError("Method 'prepare_and_start_backend' is only available on Windows.")
+
     port = _check_port_or_get_one(port)
     installations = get_available_ansys_installations()
     if product_version != None:

--- a/src/ansys/geometry/core/connection/product_instance.py
+++ b/src/ansys/geometry/core/connection/product_instance.py
@@ -17,10 +17,8 @@
     """
     return subprocess.Popen(
         args,
-        shell=os.name != "nt",
         stdin=subprocess.DEVNULL,
         stdout=subprocess.DEVNULL,
         stderr=subprocess.DEVNULL,
         env=local_env,
     )
-


Rekor's goals are to provide an immutable tamper resistant ledger of metadata generated within a software projects supply chain. A malformed proposed entry of the `intoto/v0.0.2` type can cause a panic on a thread within the Rekor process. The thread is recovered so the client receives a 500 error message and service still continues, so the availability impact of this is minimal. This has been fixed in v1.2.0 of Rekor. Users are advised to upgrade. There are no known workarounds for this vulnerability.

Reachable Assertion

The product contains an assert() or similar statement that can be triggered by an attacker, which leads to an application exit or other behavior that is more severe than necessary.

func (v V001Entry) IndexKeys() ([]string, error) {
	var result []string

	for _, sig := range v.DSSEObj.Signatures {
		keyObj, err := x509.NewPublicKey(bytes.NewReader(*sig.Verifier))
		if err != nil {
			return result, err
		}

		canonKey, err := keyObj.CanonicalValue()
		if err != nil {
			return result, fmt.Errorf("could not canonicalize key: %w", err)
		}

		keyHash := sha256.Sum256(canonKey)
		result = append(result, "sha256:"+hex.EncodeToString(keyHash[:]))

		result = append(result, keyObj.Subjects()...)
	}

	if v.DSSEObj.PayloadHash != nil {
		payloadHashKey := strings.ToLower(fmt.Sprintf("%s:%s", *v.DSSEObj.PayloadHash.Algorithm, *v.DSSEObj.PayloadHash.Value))
		result = append(result, payloadHashKey)
	}

	if v.DSSEObj.EnvelopeHash != nil {
		envelopeHashKey := strings.ToLower(fmt.Sprintf("%s:%s", *v.DSSEObj.EnvelopeHash.Algorithm, *v.DSSEObj.EnvelopeHash.Value))
		result = append(result, envelopeHashKey)
	}

	if v.env == nil {
		log.Logger.Info("DSSEObj content or DSSE envelope is nil, returning partial set of keys")
		return result, nil
	}

	switch v.env.PayloadType {
	case in_toto.PayloadType:

		if v.env.Payload == "" {
			log.Logger.Info("DSSEObj DSSE payload is empty")
			return result, nil
		}
		decodedPayload, err := v.env.DecodeB64Payload()
		if err != nil {
			return result, fmt.Errorf("could not decode envelope payload: %w", err)
		}
		statement, err := parseStatement(decodedPayload)
		if err != nil {
			return result, err
		}
		for _, s := range statement.Subject {
			for alg, ds := range s.Digest {
				result = append(result, alg+":"+ds)
			}
		}
		// Not all in-toto statements will contain a SLSA provenance predicate.
		// See https://github.com/in-toto/attestation/blob/main/spec/README.md#predicate
		// for other predicates.
		if predicate, err := parseSlsaPredicate(decodedPayload); err == nil {
			if predicate.Predicate.Materials != nil {
				for _, s := range predicate.Predicate.Materials {
					for alg, ds := range s.Digest {
						result = append(result, alg+":"+ds)
					}
				}
			}
		}
	default:
		log.Logger.Infof("Unknown DSSE envelope payloadType: %s", v.env.PayloadType)
	}
	return result, nil
}

func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
	it, ok := pe.(*models.DSSE)
	if !ok {
		return errors.New("cannot unmarshal non DSSE v0.0.1 type")
	}

	dsseObj := &models.DSSEV001Schema{}

	if err := types.DecodeEntry(it.Spec, dsseObj); err != nil {
		return err
	}

	// field validation
	if err := dsseObj.Validate(strfmt.Default); err != nil {
		return err
	}

	// either we have just proposed content or the canonicalized fields
	if dsseObj.ProposedContent == nil {
		// then we need canonicalized fields, and all must be present (if present, they would have been validated in the above call to Validate())
		if dsseObj.EnvelopeHash == nil || dsseObj.PayloadHash == nil || len(dsseObj.Signatures) == 0 {
			return errors.New("either proposedContent or envelopeHash, payloadHash, and signatures must be present")
		}
		v.DSSEObj = *dsseObj
		return nil
	}
	// if we're here, then we're trying to propose a new entry so we check to ensure client's aren't setting server-side computed fields
	if dsseObj.EnvelopeHash != nil || dsseObj.PayloadHash != nil || len(dsseObj.Signatures) != 0 {
		return errors.New("either proposedContent or envelopeHash, payloadHash, and signatures must be present but not both")
	}

	env := &dsse.Envelope{}
	if err := json.Unmarshal([]byte(*dsseObj.ProposedContent.Envelope), env); err != nil {
		return err
	}

	if len(env.Signatures) == 0 {
		return errors.New("DSSE envelope must contain 1 or more signatures")
	}

	allPubKeyBytes := make([][]byte, 0)
	for _, publicKey := range dsseObj.ProposedContent.Verifiers {
		allPubKeyBytes = append(allPubKeyBytes, publicKey)
	}

func (v V001Entry) IndexKeys() ([]string, error) {
	var result []string

	for _, sig := range v.DSSEObj.Signatures {
		if sig == nil || sig.Verifier == nil {
			return result, errors.New("missing or malformed public key")
		}
		keyObj, err := x509.NewPublicKey(bytes.NewReader(*sig.Verifier))
		if err != nil {
			return result, err
		}

		canonKey, err := keyObj.CanonicalValue()
		if err != nil {
			return result, fmt.Errorf("could not canonicalize key: %w", err)
		}

		keyHash := sha256.Sum256(canonKey)
		result = append(result, "sha256:"+hex.EncodeToString(keyHash[:]))

		result = append(result, keyObj.Subjects()...)
	}

	if v.DSSEObj.PayloadHash != nil {
		payloadHashKey := strings.ToLower(fmt.Sprintf("%s:%s", *v.DSSEObj.PayloadHash.Algorithm, *v.DSSEObj.PayloadHash.Value))
		result = append(result, payloadHashKey)
	}

	if v.DSSEObj.EnvelopeHash != nil {
		envelopeHashKey := strings.ToLower(fmt.Sprintf("%s:%s", *v.DSSEObj.EnvelopeHash.Algorithm, *v.DSSEObj.EnvelopeHash.Value))
		result = append(result, envelopeHashKey)
	}

	if v.env == nil {
		log.Logger.Info("DSSEObj content or DSSE envelope is nil, returning partial set of keys")
		return result, nil
	}

	switch v.env.PayloadType {
	case in_toto.PayloadType:

		if v.env.Payload == "" {
			log.Logger.Info("DSSEObj DSSE payload is empty")
			return result, nil
		}
		decodedPayload, err := v.env.DecodeB64Payload()
		if err != nil {
			return result, fmt.Errorf("could not decode envelope payload: %w", err)
		}
		statement, err := parseStatement(decodedPayload)
		if err != nil {
			return result, err
		}
		for _, s := range statement.Subject {
			for alg, ds := range s.Digest {
				result = append(result, alg+":"+ds)
			}
		}
		// Not all in-toto statements will contain a SLSA provenance predicate.
		// See https://github.com/in-toto/attestation/blob/main/spec/README.md#predicate
		// for other predicates.
		if predicate, err := parseSlsaPredicate(decodedPayload); err == nil {
			if predicate.Predicate.Materials != nil {
				for _, s := range predicate.Predicate.Materials {
					for alg, ds := range s.Digest {
						result = append(result, alg+":"+ds)
					}
				}
			}
		}
	default:
		log.Logger.Infof("Unknown DSSE envelope payloadType: %s", v.env.PayloadType)
	}
	return result, nil
}

func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
	it, ok := pe.(*models.DSSE)
	if !ok {
		return errors.New("cannot unmarshal non DSSE v0.0.1 type")
	}

	dsseObj := &models.DSSEV001Schema{}

	if err := types.DecodeEntry(it.Spec, dsseObj); err != nil {
		return err
	}

	// field validation
	if err := dsseObj.Validate(strfmt.Default); err != nil {
		return err
	}

	// either we have just proposed content or the canonicalized fields
	if dsseObj.ProposedContent == nil {
		// then we need canonicalized fields, and all must be present (if present, they would have been validated in the above call to Validate())
		if dsseObj.EnvelopeHash == nil || dsseObj.PayloadHash == nil || len(dsseObj.Signatures) == 0 {
			return errors.New("either proposedContent or envelopeHash, payloadHash, and signatures must be present")
		}
		v.DSSEObj = *dsseObj
		return nil
	}
	// if we're here, then we're trying to propose a new entry so we check to ensure client's aren't setting server-side computed fields
	if dsseObj.EnvelopeHash != nil || dsseObj.PayloadHash != nil || len(dsseObj.Signatures) != 0 {
		return errors.New("either proposedContent or envelopeHash, payloadHash, and signatures must be present but not both")
	}

	env := &dsse.Envelope{}
	if err := json.Unmarshal([]byte(*dsseObj.ProposedContent.Envelope), env); err != nil {
		return err
	}

	if len(env.Signatures) == 0 {
		return errors.New("DSSE envelope must contain 1 or more signatures")
	}

	allPubKeyBytes := make([][]byte, 0)
	for _, publicKey := range dsseObj.ProposedContent.Verifiers {
		if publicKey == nil {
			return errors.New("an invalid null verifier was provided in ProposedContent")
		}

		allPubKeyBytes = append(allPubKeyBytes, publicKey)
	}

--- a/pkg/types/dsse/v0.0.1/entry.go
+++ b/pkg/types/dsse/v0.0.1/entry.go
@@ -2,6 +2,9 @@
 	var result []string

 	for _, sig := range v.DSSEObj.Signatures {
+		if sig == nil || sig.Verifier == nil {
+			return result, errors.New("missing or malformed public key")
+		}
 		keyObj, err := x509.NewPublicKey(bytes.NewReader(*sig.Verifier))
 		if err != nil {
 			return result, err

--- a/pkg/types/dsse/v0.0.1/entry.go
+++ b/pkg/types/dsse/v0.0.1/entry.go
@@ -40,5 +40,9 @@

 	allPubKeyBytes := make([][]byte, 0)
 	for _, publicKey := range dsseObj.ProposedContent.Verifiers {
+		if publicKey == nil {
+			return errors.New("an invalid null verifier was provided in ProposedContent")
+		}
+
 		allPubKeyBytes = append(allPubKeyBytes, publicKey)
 	}


IPython (Interactive Python) is a command shell for interactive computing in multiple programming languages, originally developed for the Python programming language. Affected versions are subject to an arbitrary code execution vulnerability achieved by not properly managing cross user temporary files. This vulnerability allows one user to run code as another on the same machine. All users are advised to upgrade.

    def find_profile_dir_by_name(cls, ipython_dir, name=u'default', config=None):
        """Find an existing profile dir by profile name, return its ProfileDir.

        This searches through a sequence of paths for a profile dir.  If it
        is not found, a :class:`ProfileDirError` exception will be raised.

        The search path algorithm is:
        1. ``os.getcwd()``
        2. ``ipython_dir``

        Parameters
        ----------
        ipython_dir : unicode or str
            The IPython directory to use.
        name : unicode or str
            The name of the profile.  The name of the profile directory
            will be "profile_<profile>".
        """
        dirname = u'profile_' + name
        paths = [os.getcwd(), ipython_dir]
        for p in paths:
            profile_dir = os.path.join(p, dirname)
            if os.path.isdir(profile_dir):
                return cls(location=profile_dir, config=config)
        else:
            raise ProfileDirError('Profile directory not found in paths: %s' % dirname)

    def find_profile_dir_by_name(cls, ipython_dir, name=u'default', config=None):
        """Find an existing profile dir by profile name, return its ProfileDir.

        This searches through a sequence of paths for a profile dir.  If it
        is not found, a :class:`ProfileDirError` exception will be raised.

        The search path algorithm is:
        1. ``os.getcwd()`` # removed for security reason.
        2. ``ipython_dir``

        Parameters
        ----------
        ipython_dir : unicode or str
            The IPython directory to use.
        name : unicode or str
            The name of the profile.  The name of the profile directory
            will be "profile_<profile>".
        """
        dirname = u'profile_' + name
        paths = [ipython_dir]
        for p in paths:
            profile_dir = os.path.join(p, dirname)
            if os.path.isdir(profile_dir):
                return cls(location=profile_dir, config=config)
        else:
            raise ProfileDirError('Profile directory not found in paths: %s' % dirname)

--- a/IPython/core/profiledir.py
+++ b/IPython/core/profiledir.py
@@ -5,7 +5,7 @@
         is not found, a :class:`ProfileDirError` exception will be raised.

         The search path algorithm is:
-        1. ``os.getcwd()``
+        1. ``os.getcwd()`` # removed for security reason.
         2. ``ipython_dir``

         Parameters
@@ -17,7 +17,7 @@
             will be "profile_<profile>".
         """
         dirname = u'profile_' + name
-        paths = [os.getcwd(), ipython_dir]
+        paths = [ipython_dir]
         for p in paths:
             profile_dir = os.path.join(p, dirname)
             if os.path.isdir(profile_dir):

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

cd /workspace/ipython
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
/workspace/PoC_env/CVE-2022-21699/bin/python -W ignore::UserWarning -m pytest IPython/tests/cve.py

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

cd /workspace/ipython
git apply --whitespace=nowarn /workspace/fix.patch
/workspace/PoC_env/CVE-2022-21699/bin/python -m pytest -v IPython/core/tests/test_profile.py IPython/core/tests/test_application.py


In package github.com/sassoftware/go-rpmutils/cpio before version 0.1.0, the CPIO extraction functionality doesn't sanitize the paths of the archived files for leading and non-leading ".." which leads in file extraction outside of the current directory. Note: the fixing commit was applied to all affected versions which were re-released.

External Control of File Name or Path

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

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

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

func Extract(rs io.Reader, dest string) error {
	linkMap := make(map[int][]string)

	stream := NewCpioStream(rs)

	for {
		entry, err := stream.ReadNextEntry()
		if err != nil {
			return err
		}

		if entry.Header.filename == TRAILER {
			break
		}

		target := path.Join(dest, path.Clean(entry.Header.filename))
		parent := path.Dir(target)

		// Create the parent directory if it doesn't exist.
		if _, err := os.Stat(parent); os.IsNotExist(err) {
			if err := os.MkdirAll(parent, 0755); err != nil {
				return err
			}
		}

		// FIXME: Need a makedev implementation in go.

		switch entry.Header.Mode() &^ 07777 {
		case S_ISCHR:
			logger.Debug("unpacking char device")
			// FIXME: skipping due to lack of makedev.
			continue
		case S_ISBLK:
			logger.Debug("unpacking block device")
			// FIXME: skipping due to lack of makedev.
			continue
		case S_ISDIR:
			logger.Debug("unpacking dir")
			m := os.FileMode(entry.Header.Mode()).Perm()
			if err := os.Mkdir(target, m); err != nil && !os.IsExist(err) {
				return err
			}
		case S_ISFIFO:
			logger.Debug("unpacking named pipe")
			if err := fileutil.Mkfifo(target, uint32(entry.Header.Mode())); err != nil {
				return err
			}
		case S_ISLNK:
			logger.Debug("unpacking symlink")
			buf := make([]byte, entry.Header.c_filesize)
			if _, err := entry.payload.Read(buf); err != nil {
				return err
			}
			if err := os.Symlink(string(buf), target); err != nil {
				return err
			}
		case S_ISREG:
			logger.Debug("unpacking regular file")
			// save hardlinks until after the taget is written
			if entry.Header.c_nlink > 1 && entry.Header.c_filesize == 0 {
				logger.Debug("regular file is a hard link")
				l, ok := linkMap[entry.Header.c_ino]
				if !ok {
					l = make([]string, 0)
				}
				l = append(l, target)
				linkMap[entry.Header.c_ino] = l
				continue
			}

			// FIXME: Set permissions on files when creating.
			f, err := os.Create(target)
			if err != nil {
				return err
			}
			written, err := io.Copy(f, entry.payload)
			if err != nil {
				return err
			}
			if written != int64(entry.Header.c_filesize) {
				logger.Debugf("written: %d, filesize: %d", written, entry.Header.c_filesize)
				return fmt.Errorf("short write")
			}
			if err := f.Close(); err != nil {
				return err
			}

			// Create hardlinks after the file content is written.
			if entry.Header.c_nlink > 1 && entry.Header.c_filesize > 0 {
				l, ok := linkMap[entry.Header.c_ino]
				if !ok {
					return fmt.Errorf("hardlinks missing")
				}

				for _, t := range l {
					if err := os.Link(target, t); err != nil {
						return err
					}
				}
			}
		default:
			return fmt.Errorf("unknown file mode 0%o for %s",
				entry.Header.c_mode, entry.Header.filename)
		}
	}

	return nil
}

func Extract(rs io.Reader, dest string) error {
	dest = filepath.Clean(filepath.FromSlash(dest))
	linkMap := make(map[int][]string)

	stream := NewCpioStream(rs)

	for {
		entry, err := stream.ReadNextEntry()
		if err != nil {
			return err
		}

		if entry.Header.filename == TRAILER {
			break
		}

		// sanitize path
		target := path.Clean(entry.Header.filename)
		for strings.HasPrefix(target, "../") {
			target = target[3:]
		}
		target = filepath.Join(dest, filepath.FromSlash(target))
		if !strings.HasPrefix(target, dest+string(filepath.Separator)) && dest != target {
			// this shouldn't happen due to the sanitization above but always check
			return fmt.Errorf("invalid cpio path %q", entry.Header.filename)
		}
		// Create the parent directory if it doesn't exist.
		parent := filepath.Dir(target)
		if err := os.MkdirAll(parent, 0755); err != nil {
			return err
		}
		// FIXME: Need a makedev implementation in go.

		switch entry.Header.Mode() &^ 07777 {
		case S_ISCHR:
			logger.Debug("unpacking char device")
			// FIXME: skipping due to lack of makedev.
			continue
		case S_ISBLK:
			logger.Debug("unpacking block device")
			// FIXME: skipping due to lack of makedev.
			continue
		case S_ISDIR:
			logger.Debug("unpacking dir")
			m := os.FileMode(entry.Header.Mode()).Perm()
			if err := os.Mkdir(target, m); err != nil && !os.IsExist(err) {
				return err
			}
		case S_ISFIFO:
			logger.Debug("unpacking named pipe")
			if err := fileutil.Mkfifo(target, uint32(entry.Header.Mode())); err != nil {
				return err
			}
		case S_ISLNK:
			logger.Debug("unpacking symlink")
			buf := make([]byte, entry.Header.c_filesize)
			if _, err := entry.payload.Read(buf); err != nil {
				return err
			}
			if err := os.Symlink(string(buf), target); err != nil {
				return err
			}
		case S_ISREG:
			logger.Debug("unpacking regular file")
			// save hardlinks until after the taget is written
			if entry.Header.c_nlink > 1 && entry.Header.c_filesize == 0 {
				logger.Debug("regular file is a hard link")
				l, ok := linkMap[entry.Header.c_ino]
				if !ok {
					l = make([]string, 0)
				}
				l = append(l, target)
				linkMap[entry.Header.c_ino] = l
				continue
			}

			// FIXME: Set permissions on files when creating.
			f, err := os.Create(target)
			if err != nil {
				return err
			}
			written, err := io.Copy(f, entry.payload)
			if err != nil {
				return err
			}
			if written != int64(entry.Header.c_filesize) {
				logger.Debugf("written: %d, filesize: %d", written, entry.Header.c_filesize)
				return fmt.Errorf("short write")
			}
			if err := f.Close(); err != nil {
				return err
			}

			// Create hardlinks after the file content is written.
			if entry.Header.c_nlink > 1 && entry.Header.c_filesize > 0 {
				l, ok := linkMap[entry.Header.c_ino]
				if !ok {
					return fmt.Errorf("hardlinks missing")
				}

				for _, t := range l {
					if err := os.Link(target, t); err != nil {
						return err
					}
				}
			}
		default:
			return fmt.Errorf("unknown file mode 0%o for %s",
				entry.Header.c_mode, entry.Header.filename)
		}
	}

	return nil
}

--- a/cpio/extract.go
+++ b/cpio/extract.go
@@ -1,4 +1,5 @@
 func Extract(rs io.Reader, dest string) error {
+	dest = filepath.Clean(filepath.FromSlash(dest))
 	linkMap := make(map[int][]string)

 	stream := NewCpioStream(rs)
@@ -13,16 +14,21 @@
 			break
 		}

-		target := path.Join(dest, path.Clean(entry.Header.filename))
-		parent := path.Dir(target)
-
+		// sanitize path
+		target := path.Clean(entry.Header.filename)
+		for strings.HasPrefix(target, "../") {
+			target = target[3:]
+		}
+		target = filepath.Join(dest, filepath.FromSlash(target))
+		if !strings.HasPrefix(target, dest+string(filepath.Separator)) && dest != target {
+			// this shouldn't happen due to the sanitization above but always check
+			return fmt.Errorf("invalid cpio path %q", entry.Header.filename)
+		}
 		// Create the parent directory if it doesn't exist.
-		if _, err := os.Stat(parent); os.IsNotExist(err) {
-			if err := os.MkdirAll(parent, 0755); err != nil {
-				return err
-			}
+		parent := filepath.Dir(target)
+		if err := os.MkdirAll(parent, 0755); err != nil {
+			return err
 		}
-
 		// FIXME: Need a makedev implementation in go.

 		switch entry.Header.Mode() &^ 07777 {


Jupyter Remote Desktop Proxy allows you to run a Linux Desktop on a JupyterHub. jupyter-remote-desktop-proxy was meant to rely on UNIX sockets readable only by the current user since version 3.0.0, but when used with TigerVNC, the VNC server started by jupyter-remote-desktop-proxy were still accessible via the network. This vulnerability does not affect users having TurboVNC as the vncserver executable. This issue is fixed in 3.0.1.

Exposure of Resource to Wrong Sphere

The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.

def setup_websockify():
    vncserver = which('vncserver')
    if not vncserver:
        raise RuntimeError(
            "vncserver executable not found, please install a VNC server"
        )

    # {unix_socket} is expanded by jupyter-server-proxy
    vnc_args = [vncserver, '-rfbunixpath', '{unix_socket}']

    xstartup = os.getenv("JUPYTER_REMOTE_DESKTOP_PROXY_XSTARTUP")
    if not xstartup and not os.path.exists(os.path.expanduser('~/.vnc/xstartup')):
        xstartup = os.path.join(HERE, 'share/xstartup')
    if xstartup:
        vnc_args.extend(['-xstartup', xstartup])

    vnc_command = shlex.join(
        vnc_args
        + [
            '-verbose',
            '-fg',
            '-geometry',
            '1680x1050',
            '-SecurityTypes',
            'None',
        ]
    )

    return {
        'command': ['/bin/sh', '-c', f'cd {os.getcwd()} && {vnc_command}'],
        'timeout': 30,
        'new_browser_window': True,
        # We want the launcher entry to point to /desktop/, not to /desktop-websockify/
        # /desktop/ is the user facing URL, while /desktop-websockify/ now *only* serves
        # websockets.
        "launcher_entry": {"title": "Desktop", "path_info": "desktop"},
        "unix_socket": True,
        "raw_socket_proxy": True,
    }

def setup_websockify():
    vncserver = which('vncserver')
    if not vncserver:
        raise RuntimeError(
            "vncserver executable not found, please install a VNC server"
        )

    # TurboVNC and TigerVNC share the same origin and both use a Perl script
    # as the executable vncserver. We can determine if vncserver is TigerVNC
    # by searching tigervnc string in the Perl script.
    #
    # The content of the vncserver executable can differ depending on how
    # TigerVNC and TurboVNC has been distributed. Below are files known to be
    # read in some situations:
    #
    # - https://github.com/TigerVNC/tigervnc/blob/v1.13.1/unix/vncserver/vncserver.in
    # - https://github.com/TurboVNC/turbovnc/blob/3.1.1/unix/vncserver.in
    #
    with open(vncserver) as vncserver_file:
        vncserver_file_text = vncserver_file.read().casefold()
    is_turbovnc = "turbovnc" in vncserver_file_text

    # {unix_socket} is expanded by jupyter-server-proxy
    vnc_args = [vncserver, '-rfbunixpath', "{unix_socket}", "-rfbport", "-1"]
    if is_turbovnc:
        # turbovnc doesn't handle being passed -rfbport -1, but turbovnc also
        # defaults to not opening a TCP port which is what we want to ensure
        vnc_args = [vncserver, '-rfbunixpath', "{unix_socket}"]

    xstartup = os.getenv("JUPYTER_REMOTE_DESKTOP_PROXY_XSTARTUP")
    if not xstartup and not os.path.exists(os.path.expanduser('~/.vnc/xstartup')):
        xstartup = os.path.join(HERE, 'share/xstartup')
    if xstartup:
        vnc_args.extend(['-xstartup', xstartup])

    vnc_command = shlex.join(
        vnc_args
        + [
            '-verbose',
            '-fg',
            '-geometry',
            '1680x1050',
            '-SecurityTypes',
            'None',
        ]
    )

    return {
        'command': ['/bin/sh', '-c', f'cd {os.getcwd()} && {vnc_command}'],
        'timeout': 30,
        'new_browser_window': True,
        # We want the launcher entry to point to /desktop/, not to /desktop-websockify/
        # /desktop/ is the user facing URL, while /desktop-websockify/ now *only* serves
        # websockets.
        "launcher_entry": {"title": "Desktop", "path_info": "desktop"},
        "unix_socket": True,
        "raw_socket_proxy": True,
    }

--- a/jupyter_remote_desktop_proxy/setup_websockify.py
+++ b/jupyter_remote_desktop_proxy/setup_websockify.py
@@ -5,8 +5,27 @@
             "vncserver executable not found, please install a VNC server"
         )

+    # TurboVNC and TigerVNC share the same origin and both use a Perl script
+    # as the executable vncserver. We can determine if vncserver is TigerVNC
+    # by searching tigervnc string in the Perl script.
+    #
+    # The content of the vncserver executable can differ depending on how
+    # TigerVNC and TurboVNC has been distributed. Below are files known to be
+    # read in some situations:
+    #
+    # - https://github.com/TigerVNC/tigervnc/blob/v1.13.1/unix/vncserver/vncserver.in
+    # - https://github.com/TurboVNC/turbovnc/blob/3.1.1/unix/vncserver.in
+    #
+    with open(vncserver) as vncserver_file:
+        vncserver_file_text = vncserver_file.read().casefold()
+    is_turbovnc = "turbovnc" in vncserver_file_text
+
     # {unix_socket} is expanded by jupyter-server-proxy
-    vnc_args = [vncserver, '-rfbunixpath', '{unix_socket}']
+    vnc_args = [vncserver, '-rfbunixpath', "{unix_socket}", "-rfbport", "-1"]
+    if is_turbovnc:
+        # turbovnc doesn't handle being passed -rfbport -1, but turbovnc also
+        # defaults to not opening a TCP port which is what we want to ensure
+        vnc_args = [vncserver, '-rfbunixpath', "{unix_socket}"]

     xstartup = os.getenv("JUPYTER_REMOTE_DESKTOP_PROXY_XSTARTUP")
     if not xstartup and not os.path.exists(os.path.expanduser('~/.vnc/xstartup')):