aiohttp is an asynchronous HTTP client/server framework for asyncio and Python. In aiohttp before version 3.7.4 there is an open redirect vulnerability. A maliciously crafted link to an aiohttp-based web-server could redirect the browser to a different website. It is caused by a bug in the `aiohttp.web_middlewares.normalize_path_middleware` middleware. This security problem has been fixed in 3.7.4. Upgrade your dependency using pip as follows "pip install aiohttp >= 3.7.4". If upgrading is not an option for you, a workaround can be to avoid using `aiohttp.web_middlewares.normalize_path_middleware` in your applications.

URL Redirection to Untrusted Site ('Open Redirect')

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

    async def impl(request: Request, handler: _Handler) -> StreamResponse:
        if isinstance(request.match_info.route, SystemRoute):
            paths_to_check = []
            if "?" in request.raw_path:
                path, query = request.raw_path.split("?", 1)
                query = "?" + query
            else:
                query = ""
                path = request.raw_path

            if merge_slashes:
                paths_to_check.append(re.sub("//+", "/", path))
            if append_slash and not request.path.endswith("/"):
                paths_to_check.append(path + "/")
            if remove_slash and request.path.endswith("/"):
                paths_to_check.append(path[:-1])
            if merge_slashes and append_slash:
                paths_to_check.append(re.sub("//+", "/", path + "/"))
            if merge_slashes and remove_slash and path.endswith("/"):
                merged_slashes = re.sub("//+", "/", path)
                paths_to_check.append(merged_slashes[:-1])

            for path in paths_to_check:
                resolves, request = await _check_request_resolves(request, path)
                if resolves:
                    raise redirect_class(request.raw_path + query)

        return await handler(request)

    return impl

    async def impl(request: Request, handler: _Handler) -> StreamResponse:
        if isinstance(request.match_info.route, SystemRoute):
            paths_to_check = []
            if "?" in request.raw_path:
                path, query = request.raw_path.split("?", 1)
                query = "?" + query
            else:
                query = ""
                path = request.raw_path

            if merge_slashes:
                paths_to_check.append(re.sub("//+", "/", path))
            if append_slash and not request.path.endswith("/"):
                paths_to_check.append(path + "/")
            if remove_slash and request.path.endswith("/"):
                paths_to_check.append(path[:-1])
            if merge_slashes and append_slash:
                paths_to_check.append(re.sub("//+", "/", path + "/"))
            if merge_slashes and remove_slash and path.endswith("/"):
                merged_slashes = re.sub("//+", "/", path)
                paths_to_check.append(merged_slashes[:-1])

            for path in paths_to_check:
                path = re.sub("^//+", "/", path)  # SECURITY: GHSA-v6wp-4m6f-gcjg
                resolves, request = await _check_request_resolves(request, path)
                if resolves:
                    raise redirect_class(request.raw_path + query)

        return await handler(request)

    return impl

--- a/aiohttp/web_middlewares.py
+++ b/aiohttp/web_middlewares.py
@@ -21,6 +21,7 @@
                 paths_to_check.append(merged_slashes[:-1])

             for path in paths_to_check:
+                path = re.sub("^//+", "/", path)  # SECURITY: GHSA-v6wp-4m6f-gcjg
                 resolves, request = await _check_request_resolves(request, path)
                 if resolves:
                     raise redirect_class(request.raw_path + query)


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

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

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

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


containerd is an open source container runtime. A bug was found in containerd prior to versions 1.6.18 and 1.5.18 where supplementary groups are not set up properly inside a container. If an attacker has direct access to a container and manipulates their supplementary group access, they may be able to use supplementary group access to bypass primary group restrictions in some cases, potentially gaining access to sensitive information or gaining the ability to execute code in that container. Downstream applications that use the containerd client library may be affected as well.

This bug has been fixed in containerd v1.6.18 and v.1.5.18. Users should update to these versions and recreate containers to resolve this issue. Users who rely on a downstream application that uses containerd's client library should check that application for a separate advisory and instructions. As a workaround, ensure that the `"USER $USERNAME"` Dockerfile instruction is not used. Instead, set the container entrypoint to a value similar to `ENTRYPOINT ["su", "-", "user"]` to allow `su` to properly set up supplementary groups.

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.

func WithUser(userstr string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
		setProcess(s)

		// For LCOW it's a bit harder to confirm that the user actually exists on the host as a rootfs isn't
		// mounted on the host and shared into the guest, but rather the rootfs is constructed entirely in the
		// guest itself. To accommodate this, a spot to place the user string provided by a client as-is is needed.
		// The `Username` field on the runtime spec is marked by Platform as only for Windows, and in this case it
		// *is* being set on a Windows host at least, but will be used as a temporary holding spot until the guest
		// can use the string to perform these same operations to grab the uid:gid inside.
		if s.Windows != nil && s.Linux != nil {
			s.Process.User.Username = userstr
			return nil
		}

		parts := strings.Split(userstr, ":")
		switch len(parts) {
		case 1:
			v, err := strconv.Atoi(parts[0])
			if err != nil {
				// if we cannot parse as a uint they try to see if it is a username
				return WithUsername(userstr)(ctx, client, c, s)
			}
			return WithUserID(uint32(v))(ctx, client, c, s)
		case 2:
			var (
				username  string
				groupname string
			)
			var uid, gid uint32
			v, err := strconv.Atoi(parts[0])
			if err != nil {
				username = parts[0]
			} else {
				uid = uint32(v)
			}
			if v, err = strconv.Atoi(parts[1]); err != nil {
				groupname = parts[1]
			} else {
				gid = uint32(v)
			}
			if username == "" && groupname == "" {
				s.Process.User.UID, s.Process.User.GID = uid, gid
				return nil
			}
			f := func(root string) error {
				if username != "" {
					user, err := UserFromPath(root, func(u user.User) bool {
						return u.Name == username
					})
					if err != nil {
						return err
					}
					uid = uint32(user.Uid)
				}
				if groupname != "" {
					gid, err = GIDFromPath(root, func(g user.Group) bool {
						return g.Name == groupname
					})
					if err != nil {
						return err
					}
				}
				s.Process.User.UID, s.Process.User.GID = uid, gid
				return nil
			}
			if c.Snapshotter == "" && c.SnapshotKey == "" {
				if !isRootfsAbs(s.Root.Path) {
					return errors.New("rootfs absolute path is required")
				}
				return f(s.Root.Path)
			}
			if c.Snapshotter == "" {
				return errors.New("no snapshotter set for container")
			}
			if c.SnapshotKey == "" {
				return errors.New("rootfs snapshot not created for container")
			}
			snapshotter := client.SnapshotService(c.Snapshotter)
			mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
			if err != nil {
				return err
			}

			mounts = tryReadonlyMounts(mounts)
			return mount.WithTempMount(ctx, mounts, f)
		default:
			return fmt.Errorf("invalid USER value %s", userstr)
		}
	}
}

func WithUIDGID(uid, gid uint32) SpecOpts {
	return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
		setProcess(s)
		s.Process.User.UID = uid
		s.Process.User.GID = gid
		return nil
	}
}

func WithUserID(uid uint32) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		setProcess(s)
		if c.Snapshotter == "" && c.SnapshotKey == "" {
			if !isRootfsAbs(s.Root.Path) {
				return errors.New("rootfs absolute path is required")
			}
			user, err := UserFromPath(s.Root.Path, func(u user.User) bool {
				return u.Uid == int(uid)
			})
			if err != nil {
				if os.IsNotExist(err) || err == ErrNoUsersFound {
					s.Process.User.UID, s.Process.User.GID = uid, 0
					return nil
				}
				return err
			}
			s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
			return nil

		}
		if c.Snapshotter == "" {
			return errors.New("no snapshotter set for container")
		}
		if c.SnapshotKey == "" {
			return errors.New("rootfs snapshot not created for container")
		}
		snapshotter := client.SnapshotService(c.Snapshotter)
		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
		if err != nil {
			return err
		}

		mounts = tryReadonlyMounts(mounts)
		return mount.WithTempMount(ctx, mounts, func(root string) error {
			user, err := UserFromPath(root, func(u user.User) bool {
				return u.Uid == int(uid)
			})
			if err != nil {
				if os.IsNotExist(err) || err == ErrNoUsersFound {
					s.Process.User.UID, s.Process.User.GID = uid, 0
					return nil
				}
				return err
			}
			s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
			return nil
		})
	}
}

func WithUsername(username string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		setProcess(s)
		if s.Linux != nil {
			if c.Snapshotter == "" && c.SnapshotKey == "" {
				if !isRootfsAbs(s.Root.Path) {
					return errors.New("rootfs absolute path is required")
				}
				user, err := UserFromPath(s.Root.Path, func(u user.User) bool {
					return u.Name == username
				})
				if err != nil {
					return err
				}
				s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
				return nil
			}
			if c.Snapshotter == "" {
				return errors.New("no snapshotter set for container")
			}
			if c.SnapshotKey == "" {
				return errors.New("rootfs snapshot not created for container")
			}
			snapshotter := client.SnapshotService(c.Snapshotter)
			mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
			if err != nil {
				return err
			}

			mounts = tryReadonlyMounts(mounts)
			return mount.WithTempMount(ctx, mounts, func(root string) error {
				user, err := UserFromPath(root, func(u user.User) bool {
					return u.Name == username
				})
				if err != nil {
					return err
				}
				s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
				return nil
			})
		} else if s.Windows != nil {
			s.Process.User.Username = username
		} else {
			return errors.New("spec does not contain Linux or Windows section")
		}
		return nil
	}
}

func WithAdditionalGIDs(userstr string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		// For LCOW or on Darwin additional GID's not supported
		if s.Windows != nil || runtime.GOOS == "darwin" {
			return nil
		}
		setProcess(s)
		setAdditionalGids := func(root string) error {
			var username string
			uid, err := strconv.Atoi(userstr)
			if err == nil {
				user, err := UserFromPath(root, func(u user.User) bool {
					return u.Uid == uid
				})
				if err != nil {
					if os.IsNotExist(err) || err == ErrNoUsersFound {
						return nil
					}
					return err
				}
				username = user.Name
			} else {
				username = userstr
			}
			gids, err := getSupplementalGroupsFromPath(root, func(g user.Group) bool {
				// we only want supplemental groups
				if g.Name == username {
					return false
				}
				for _, entry := range g.List {
					if entry == username {
						return true
					}
				}
				return false
			})
			if err != nil {
				if os.IsNotExist(err) {
					return nil
				}
				return err
			}
			s.Process.User.AdditionalGids = gids
			return nil
		}
		if c.Snapshotter == "" && c.SnapshotKey == "" {
			if !isRootfsAbs(s.Root.Path) {
				return errors.New("rootfs absolute path is required")
			}
			return setAdditionalGids(s.Root.Path)
		}
		if c.Snapshotter == "" {
			return errors.New("no snapshotter set for container")
		}
		if c.SnapshotKey == "" {
			return errors.New("rootfs snapshot not created for container")
		}
		snapshotter := client.SnapshotService(c.Snapshotter)
		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
		if err != nil {
			return err
		}

		mounts = tryReadonlyMounts(mounts)
		return mount.WithTempMount(ctx, mounts, setAdditionalGids)
	}
}

// ensureAdditionalGids ensures that the primary GID is also included in the additional GID list.
func ensureAdditionalGids(s *Spec) {
	setProcess(s)
	for _, f := range s.Process.User.AdditionalGids {
		if f == s.Process.User.GID {
			return
		}
	}
	s.Process.User.AdditionalGids = append([]uint32{s.Process.User.GID}, s.Process.User.AdditionalGids...)
}

func WithUser(userstr string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
		defer ensureAdditionalGids(s)
		setProcess(s)
		s.Process.User.AdditionalGids = nil

		// For LCOW it's a bit harder to confirm that the user actually exists on the host as a rootfs isn't
		// mounted on the host and shared into the guest, but rather the rootfs is constructed entirely in the
		// guest itself. To accommodate this, a spot to place the user string provided by a client as-is is needed.
		// The `Username` field on the runtime spec is marked by Platform as only for Windows, and in this case it
		// *is* being set on a Windows host at least, but will be used as a temporary holding spot until the guest
		// can use the string to perform these same operations to grab the uid:gid inside.
		if s.Windows != nil && s.Linux != nil {
			s.Process.User.Username = userstr
			return nil
		}

		parts := strings.Split(userstr, ":")
		switch len(parts) {
		case 1:
			v, err := strconv.Atoi(parts[0])
			if err != nil {
				// if we cannot parse as a uint they try to see if it is a username
				return WithUsername(userstr)(ctx, client, c, s)
			}
			return WithUserID(uint32(v))(ctx, client, c, s)
		case 2:
			var (
				username  string
				groupname string
			)
			var uid, gid uint32
			v, err := strconv.Atoi(parts[0])
			if err != nil {
				username = parts[0]
			} else {
				uid = uint32(v)
			}
			if v, err = strconv.Atoi(parts[1]); err != nil {
				groupname = parts[1]
			} else {
				gid = uint32(v)
			}
			if username == "" && groupname == "" {
				s.Process.User.UID, s.Process.User.GID = uid, gid
				return nil
			}
			f := func(root string) error {
				if username != "" {
					user, err := UserFromPath(root, func(u user.User) bool {
						return u.Name == username
					})
					if err != nil {
						return err
					}
					uid = uint32(user.Uid)
				}
				if groupname != "" {
					gid, err = GIDFromPath(root, func(g user.Group) bool {
						return g.Name == groupname
					})
					if err != nil {
						return err
					}
				}
				s.Process.User.UID, s.Process.User.GID = uid, gid
				return nil
			}
			if c.Snapshotter == "" && c.SnapshotKey == "" {
				if !isRootfsAbs(s.Root.Path) {
					return errors.New("rootfs absolute path is required")
				}
				return f(s.Root.Path)
			}
			if c.Snapshotter == "" {
				return errors.New("no snapshotter set for container")
			}
			if c.SnapshotKey == "" {
				return errors.New("rootfs snapshot not created for container")
			}
			snapshotter := client.SnapshotService(c.Snapshotter)
			mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
			if err != nil {
				return err
			}

			mounts = tryReadonlyMounts(mounts)
			return mount.WithTempMount(ctx, mounts, f)
		default:
			return fmt.Errorf("invalid USER value %s", userstr)
		}
	}
}

func WithUIDGID(uid, gid uint32) SpecOpts {
	return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
		defer ensureAdditionalGids(s)
		setProcess(s)
		s.Process.User.AdditionalGids = nil
		s.Process.User.UID = uid
		s.Process.User.GID = gid
		return nil
	}
}

func WithUserID(uid uint32) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		defer ensureAdditionalGids(s)
		setProcess(s)
		s.Process.User.AdditionalGids = nil
		setUser := func(root string) error {
			user, err := UserFromPath(root, func(u user.User) bool {
				return u.Uid == int(uid)
			})
			if err != nil {
				if os.IsNotExist(err) || err == ErrNoUsersFound {
					s.Process.User.UID, s.Process.User.GID = uid, 0
					return nil
				}
				return err
			}
			s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
			return nil
		}
		if c.Snapshotter == "" && c.SnapshotKey == "" {
			if !isRootfsAbs(s.Root.Path) {
				return errors.New("rootfs absolute path is required")
			}
			return setUser(s.Root.Path)
		}
		if c.Snapshotter == "" {
			return errors.New("no snapshotter set for container")
		}
		if c.SnapshotKey == "" {
			return errors.New("rootfs snapshot not created for container")
		}
		snapshotter := client.SnapshotService(c.Snapshotter)
		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
		if err != nil {
			return err
		}

		mounts = tryReadonlyMounts(mounts)
		return mount.WithTempMount(ctx, mounts, setUser)
	}
}

func WithUsername(username string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		defer ensureAdditionalGids(s)
		setProcess(s)
		s.Process.User.AdditionalGids = nil
		if s.Linux != nil {
			setUser := func(root string) error {
				user, err := UserFromPath(root, func(u user.User) bool {
					return u.Name == username
				})
				if err != nil {
					return err
				}
				s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
				return nil
			}
			if c.Snapshotter == "" && c.SnapshotKey == "" {
				if !isRootfsAbs(s.Root.Path) {
					return errors.New("rootfs absolute path is required")
				}
				return setUser(s.Root.Path)
			}
			if c.Snapshotter == "" {
				return errors.New("no snapshotter set for container")
			}
			if c.SnapshotKey == "" {
				return errors.New("rootfs snapshot not created for container")
			}
			snapshotter := client.SnapshotService(c.Snapshotter)
			mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
			if err != nil {
				return err
			}

			mounts = tryReadonlyMounts(mounts)
			return mount.WithTempMount(ctx, mounts, setUser)
		} else if s.Windows != nil {
			s.Process.User.Username = username
		} else {
			return errors.New("spec does not contain Linux or Windows section")
		}
		return nil
	}
}

func WithAdditionalGIDs(userstr string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		// For LCOW or on Darwin additional GID's not supported
		if s.Windows != nil || runtime.GOOS == "darwin" {
			return nil
		}
		setProcess(s)
		s.Process.User.AdditionalGids = nil
		setAdditionalGids := func(root string) error {
			defer ensureAdditionalGids(s)
			var username string
			uid, err := strconv.Atoi(userstr)
			if err == nil {
				user, err := UserFromPath(root, func(u user.User) bool {
					return u.Uid == uid
				})
				if err != nil {
					if os.IsNotExist(err) || err == ErrNoUsersFound {
						return nil
					}
					return err
				}
				username = user.Name
			} else {
				username = userstr
			}
			gids, err := getSupplementalGroupsFromPath(root, func(g user.Group) bool {
				// we only want supplemental groups
				if g.Name == username {
					return false
				}
				for _, entry := range g.List {
					if entry == username {
						return true
					}
				}
				return false
			})
			if err != nil {
				if os.IsNotExist(err) {
					return nil
				}
				return err
			}
			s.Process.User.AdditionalGids = gids
			return nil
		}
		if c.Snapshotter == "" && c.SnapshotKey == "" {
			if !isRootfsAbs(s.Root.Path) {
				return errors.New("rootfs absolute path is required")
			}
			return setAdditionalGids(s.Root.Path)
		}
		if c.Snapshotter == "" {
			return errors.New("no snapshotter set for container")
		}
		if c.SnapshotKey == "" {
			return errors.New("rootfs snapshot not created for container")
		}
		snapshotter := client.SnapshotService(c.Snapshotter)
		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
		if err != nil {
			return err
		}

		mounts = tryReadonlyMounts(mounts)
		return mount.WithTempMount(ctx, mounts, setAdditionalGids)
	}
}

func WithAppendAdditionalGroups(groups ...string) SpecOpts {
	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
		// For LCOW or on Darwin additional GID's are not supported
		if s.Windows != nil || runtime.GOOS == "darwin" {
			return nil
		}
		setProcess(s)
		setAdditionalGids := func(root string) error {
			defer ensureAdditionalGids(s)
			gpath, err := fs.RootPath(root, "/etc/group")
			if err != nil {
				return err
			}
			ugroups, err := user.ParseGroupFile(gpath)
			if err != nil {
				return err
			}
			groupMap := make(map[string]user.Group)
			for _, group := range ugroups {
				groupMap[group.Name] = group
			}
			var gids []uint32
			for _, group := range groups {
				gid, err := strconv.ParseUint(group, 10, 32)
				if err == nil {
					gids = append(gids, uint32(gid))
				} else {
					g, ok := groupMap[group]
					if !ok {
						return fmt.Errorf("unable to find group %s", group)
					}
					gids = append(gids, uint32(g.Gid))
				}
			}
			s.Process.User.AdditionalGids = append(s.Process.User.AdditionalGids, gids...)
			return nil
		}
		if c.Snapshotter == "" && c.SnapshotKey == "" {
			if !filepath.IsAbs(s.Root.Path) {
				return errors.New("rootfs absolute path is required")
			}
			return setAdditionalGids(s.Root.Path)
		}
		if c.Snapshotter == "" {
			return errors.New("no snapshotter set for container")
		}
		if c.SnapshotKey == "" {
			return errors.New("rootfs snapshot not created for container")
		}
		snapshotter := client.SnapshotService(c.Snapshotter)
		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
		if err != nil {
			return err
		}

		mounts = tryReadonlyMounts(mounts)
		return mount.WithTempMount(ctx, mounts, setAdditionalGids)
	}
}

--- a/oci/spec_opts.go
+++ b/oci/spec_opts.go
@@ -1,6 +1,8 @@
 func WithUser(userstr string) SpecOpts {
 	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
+		defer ensureAdditionalGids(s)
 		setProcess(s)
+		s.Process.User.AdditionalGids = nil

 		// For LCOW it's a bit harder to confirm that the user actually exists on the host as a rootfs isn't
 		// mounted on the host and shared into the guest, but rather the rootfs is constructed entirely in the

--- a/oci/spec_opts.go
+++ b/oci/spec_opts.go
@@ -1,6 +1,8 @@
 func WithUIDGID(uid, gid uint32) SpecOpts {
 	return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
+		defer ensureAdditionalGids(s)
 		setProcess(s)
+		s.Process.User.AdditionalGids = nil
 		s.Process.User.UID = uid
 		s.Process.User.GID = gid
 		return nil

--- a/oci/spec_opts.go
+++ b/oci/spec_opts.go
@@ -1,11 +1,10 @@
 func WithUserID(uid uint32) SpecOpts {
 	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
+		defer ensureAdditionalGids(s)
 		setProcess(s)
-		if c.Snapshotter == "" && c.SnapshotKey == "" {
-			if !isRootfsAbs(s.Root.Path) {
-				return errors.New("rootfs absolute path is required")
-			}
-			user, err := UserFromPath(s.Root.Path, func(u user.User) bool {
+		s.Process.User.AdditionalGids = nil
+		setUser := func(root string) error {
+			user, err := UserFromPath(root, func(u user.User) bool {
 				return u.Uid == int(uid)
 			})
 			if err != nil {
@@ -17,7 +16,12 @@
 			}
 			s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
 			return nil
-
+		}
+		if c.Snapshotter == "" && c.SnapshotKey == "" {
+			if !isRootfsAbs(s.Root.Path) {
+				return errors.New("rootfs absolute path is required")
+			}
+			return setUser(s.Root.Path)
 		}
 		if c.Snapshotter == "" {
 			return errors.New("no snapshotter set for container")
@@ -32,19 +36,6 @@
 		}

 		mounts = tryReadonlyMounts(mounts)
-		return mount.WithTempMount(ctx, mounts, func(root string) error {
-			user, err := UserFromPath(root, func(u user.User) bool {
-				return u.Uid == int(uid)
-			})
-			if err != nil {
-				if os.IsNotExist(err) || err == ErrNoUsersFound {
-					s.Process.User.UID, s.Process.User.GID = uid, 0
-					return nil
-				}
-				return err
-			}
-			s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
-			return nil
-		})
+		return mount.WithTempMount(ctx, mounts, setUser)
 	}
 }

--- a/oci/spec_opts.go
+++ b/oci/spec_opts.go
@@ -1,12 +1,11 @@
 func WithUsername(username string) SpecOpts {
 	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
+		defer ensureAdditionalGids(s)
 		setProcess(s)
+		s.Process.User.AdditionalGids = nil
 		if s.Linux != nil {
-			if c.Snapshotter == "" && c.SnapshotKey == "" {
-				if !isRootfsAbs(s.Root.Path) {
-					return errors.New("rootfs absolute path is required")
-				}
-				user, err := UserFromPath(s.Root.Path, func(u user.User) bool {
+			setUser := func(root string) error {
+				user, err := UserFromPath(root, func(u user.User) bool {
 					return u.Name == username
 				})
 				if err != nil {
@@ -14,6 +13,12 @@
 				}
 				s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
 				return nil
+			}
+			if c.Snapshotter == "" && c.SnapshotKey == "" {
+				if !isRootfsAbs(s.Root.Path) {
+					return errors.New("rootfs absolute path is required")
+				}
+				return setUser(s.Root.Path)
 			}
 			if c.Snapshotter == "" {
 				return errors.New("no snapshotter set for container")
@@ -28,16 +33,7 @@
 			}

 			mounts = tryReadonlyMounts(mounts)
-			return mount.WithTempMount(ctx, mounts, func(root string) error {
-				user, err := UserFromPath(root, func(u user.User) bool {
-					return u.Name == username
-				})
-				if err != nil {
-					return err
-				}
-				s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
-				return nil
-			})
+			return mount.WithTempMount(ctx, mounts, setUser)
 		} else if s.Windows != nil {
 			s.Process.User.Username = username
 		} else {

--- a/oci/spec_opts.go
+++ b/oci/spec_opts.go
@@ -5,7 +5,9 @@
 			return nil
 		}
 		setProcess(s)
+		s.Process.User.AdditionalGids = nil
 		setAdditionalGids := func(root string) error {
+			defer ensureAdditionalGids(s)
 			var username string
 			uid, err := strconv.Atoi(userstr)
 			if err == nil {

--- /dev/null
+++ b/oci/spec_opts.go
@@ -0,0 +1,10 @@
+// ensureAdditionalGids ensures that the primary GID is also included in the additional GID list.
+func ensureAdditionalGids(s *Spec) {
+	setProcess(s)
+	for _, f := range s.Process.User.AdditionalGids {
+		if f == s.Process.User.GID {
+			return
+		}
+	}
+	s.Process.User.AdditionalGids = append([]uint32{s.Process.User.GID}, s.Process.User.AdditionalGids...)
+}

--- /dev/null
+++ b/oci/spec_opts.go
@@ -0,0 +1,59 @@
+func WithAppendAdditionalGroups(groups ...string) SpecOpts {
+	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
+		// For LCOW or on Darwin additional GID's are not supported
+		if s.Windows != nil || runtime.GOOS == "darwin" {
+			return nil
+		}
+		setProcess(s)
+		setAdditionalGids := func(root string) error {
+			defer ensureAdditionalGids(s)
+			gpath, err := fs.RootPath(root, "/etc/group")
+			if err != nil {
+				return err
+			}
+			ugroups, err := user.ParseGroupFile(gpath)
+			if err != nil {
+				return err
+			}
+			groupMap := make(map[string]user.Group)
+			for _, group := range ugroups {
+				groupMap[group.Name] = group
+			}
+			var gids []uint32
+			for _, group := range groups {
+				gid, err := strconv.ParseUint(group, 10, 32)
+				if err == nil {
+					gids = append(gids, uint32(gid))
+				} else {
+					g, ok := groupMap[group]
+					if !ok {
+						return fmt.Errorf("unable to find group %s", group)
+					}
+					gids = append(gids, uint32(g.Gid))
+				}
+			}
+			s.Process.User.AdditionalGids = append(s.Process.User.AdditionalGids, gids...)
+			return nil
+		}
+		if c.Snapshotter == "" && c.SnapshotKey == "" {
+			if !filepath.IsAbs(s.Root.Path) {
+				return errors.New("rootfs absolute path is required")
+			}
+			return setAdditionalGids(s.Root.Path)
+		}
+		if c.Snapshotter == "" {
+			return errors.New("no snapshotter set for container")
+		}
+		if c.SnapshotKey == "" {
+			return errors.New("rootfs snapshot not created for container")
+		}
+		snapshotter := client.SnapshotService(c.Snapshotter)
+		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
+		if err != nil {
+			return err
+		}
+
+		mounts = tryReadonlyMounts(mounts)
+		return mount.WithTempMount(ctx, mounts, setAdditionalGids)
+	}
+}

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

cd /workspace/containerd
rm -rf ./oci/spec_opts_linux_test.go
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
go test  -timeout 30s -run ^TestWithAdditionalGIDs$ github.com/containerd/containerd/oci

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

cd /workspace/containerd
git apply --whitespace=nowarn /workspace/fix.patch
cd oci && go test  -timeout 30s  github.com/containerd/containerd/oci


The leafInfo.match() function in Beego v2.0.3 and below uses path.join() to deal with wildcardvalues which can lead to cross directory risk.

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 (t *Tree) Match(pattern string, ctx *context.Context) (runObject interface{}) {
	if pattern == "" || pattern[0] != '/' {
		return nil
	}
	w := make([]string, 0, 20)
	return t.match(pattern[1:], pattern, w, ctx)
}

func (t *Tree) Match(pattern string, ctx *context.Context) (runObject interface{}) {
	// fix issue 4961, deal with "./ ../ //"
	pattern = path.Clean(pattern)
	if pattern == "" || pattern[0] != '/' {
		return nil
	}
	w := make([]string, 0, 20)
	return t.match(pattern[1:], pattern, w, ctx)
}

--- a/server/web/tree.go
+++ b/server/web/tree.go
@@ -1,4 +1,6 @@
 func (t *Tree) Match(pattern string, ctx *context.Context) (runObject interface{}) {
+	// fix issue 4961, deal with "./ ../ //"
+	pattern = path.Clean(pattern)
 	if pattern == "" || pattern[0] != '/' {
 		return nil
 	}


The @ronomon/opened library before 1.5.2 is vulnerable to a command injection vulnerability which would allow a remote attacker to execute commands on the system if the library was used with untrusted 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.

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.

Unix.files = function(paths, end) {
  var self = this;
  assertPaths('paths', paths);
  assertFunction('end', end);
  var files = {};
  var queue = new Queue(1); // Concurrency yields no improvement with lsof.
  queue.onData = function(paths, end) {
    var escapedPaths = paths.map(
      function(path) {
        return '"' + path.replace(/"/g, '\\"') + '"';
      }
    );
    var command = 'lsof -F n -- ' + escapedPaths.join(' ');
    var options = {
      encoding: 'utf-8',
      maxBuffer: 2 * 1024 * 1024
    };
    Node.child.exec(command, options,
      function(error, stdout, stderr) {
        // lsof returns an error and a status code of 1 if a file is not open:
        if (error && error.code === 1 && stderr.length === 0) error = undefined;
        if (error) {
          if (/No such file or directory/i.test(stderr)) {
            error.code = 'ENOENT';
          }
          return end(error);
        }
        var lines = stdout.split('\n');
        for (var index = 0, length = lines.length; index < length; index++) {
          var line = lines[index];
          if (line[0] != 'n') continue;
          var candidate = self.unescape(line.slice(1));
          if (files.hasOwnProperty(candidate)) files[candidate] = true;
        }
        end();
      }
    );
  };

Unix.files = function(paths, end) {
  var self = this;
  assertPaths('paths', paths);
  assertFunction('end', end);
  var files = {};
  var queue = new Queue(1); // Concurrency yields no improvement with lsof.
  queue.onData = function(paths, end) {
    var command = 'lsof';
    var args = ['-F', 'n', '--'].concat(paths);
    var options = {
      encoding: 'utf-8',
      maxBuffer: 2 * 1024 * 1024
    };
    Node.child.execFile(command, args, options,
      function(error, stdout, stderr) {
        // lsof returns an error and a status code of 1 if a file is not open:
        if (error && error.code === 1 && stderr.length === 0) error = undefined;
        if (error) {
          if (/No such file or directory/i.test(stderr)) {
            error.code = 'ENOENT';
          }
          return end(error);
        }
        var lines = stdout.split('\n');
        for (var index = 0, length = lines.length; index < length; index++) {
          var line = lines[index];
          if (line[0] != 'n') continue;
          var candidate = self.unescape(line.slice(1));
          if (files.hasOwnProperty(candidate)) files[candidate] = true;
        }
        end();
      }
    );
  };

--- a/index.js
+++ b/index.js
@@ -5,17 +5,13 @@
   var files = {};
   var queue = new Queue(1); // Concurrency yields no improvement with lsof.
   queue.onData = function(paths, end) {
-    var escapedPaths = paths.map(
-      function(path) {
-        return '"' + path.replace(/"/g, '\\"') + '"';
-      }
-    );
-    var command = 'lsof -F n -- ' + escapedPaths.join(' ');
+    var command = 'lsof';
+    var args = ['-F', 'n', '--'].concat(paths);
     var options = {
       encoding: 'utf-8',
       maxBuffer: 2 * 1024 * 1024
     };
-    Node.child.exec(command, options,
+    Node.child.execFile(command, args, options,
       function(error, stdout, stderr) {
         // lsof returns an error and a status code of 1 if a file is not open:
         if (error && error.code === 1 && stderr.length === 0) error = undefined;


The npm package "tar" (aka node-tar) before versions 4.4.18, 5.0.10, and 6.1.9 has an arbitrary file creation/overwrite and arbitrary code execution vulnerability. node-tar aims to guarantee that any file whose location would be modified by a symbolic link is not extracted. This is, in part, achieved by ensuring that extracted directories are not symlinks. Additionally, in order to prevent unnecessary stat calls to determine whether a given path is a directory, paths are cached when directories are created. This logic was insufficient when extracting tar files that contained both a directory and a symlink with names containing unicode values that normalized to the same value. Additionally, on Windows systems, long path portions would resolve to the same file system entities as their 8.3 "short path" counterparts. A specially crafted tar archive could thus include a directory with one form of the path, followed by a symbolic link with a different string that resolves to the same file system entity, followed by a file using the first form. By first creating a directory, and then replacing that directory with a symlink that had a different apparent name that resolved to the same entry in the filesystem, it was thus possible to bypass node-tar symlink checks on directories, essentially allowing an untrusted tar file to symlink into an arbitrary location and subsequently extracting arbitrary files into that location, thus allowing arbitrary file creation and overwrite. These issues were addressed in releases 4.4.18, 5.0.10 and 6.1.9. The v3 branch of node-tar has been deprecated and did not receive patches for these issues. If you are still using a v3 release we recommend you update to a more recent version of node-tar. If this is not possible, a workaround is available in the referenced GHSA-qq89-hq3f-393p.

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.

module.exports = () => {
  // path => [function or Set]
  // A Set object means a directory reservation
  // A fn is a direct reservation on that path
  const queues = new Map()

  // fn => {paths:[path,...], dirs:[path, ...]}
  const reservations = new Map()

  // return a set of parent dirs for a given path
  const getDirs = path =>
    path.split('/').slice(0, -1).reduce((set, path) =>
      set.length ? set.concat(normPath(join(set[set.length - 1], path)))
      : [path], [])

  // functions currently running
  const running = new Set()

  // return the queues for each path the function cares about
  // fn => {paths, dirs}
  const getQueues = fn => {
    const res = reservations.get(fn)
    /* istanbul ignore if - unpossible */
    if (!res)
      throw new Error('function does not have any path reservations')
    return {
      paths: res.paths.map(path => queues.get(path)),
      dirs: [...res.dirs].map(path => queues.get(path)),
    }
  }

  // check if fn is first in line for all its paths, and is
  // included in the first set for all its dir queues
  const check = fn => {
    const {paths, dirs} = getQueues(fn)
    return paths.every(q => q[0] === fn) &&
      dirs.every(q => q[0] instanceof Set && q[0].has(fn))
  }

  // run the function if it's first in line and not already running
  const run = fn => {
    if (running.has(fn) || !check(fn))
      return false
    running.add(fn)
    fn(() => clear(fn))
    return true
  }

  const clear = fn => {
    if (!running.has(fn))
      return false

    const { paths, dirs } = reservations.get(fn)
    const next = new Set()

    paths.forEach(path => {
      const q = queues.get(path)
      assert.equal(q[0], fn)
      if (q.length === 1)
        queues.delete(path)
      else {
        q.shift()
        if (typeof q[0] === 'function')
          next.add(q[0])
        else
          q[0].forEach(fn => next.add(fn))
      }
    })

    dirs.forEach(dir => {
      const q = queues.get(dir)
      assert(q[0] instanceof Set)
      if (q[0].size === 1 && q.length === 1) {
        queues.delete(dir)
      } else if (q[0].size === 1) {
        q.shift()

        // must be a function or else the Set would've been reused
        next.add(q[0])
      } else
        q[0].delete(fn)
    })
    running.delete(fn)

    next.forEach(fn => run(fn))
    return true
  }

  const reserve = (paths, fn) => {
    paths = paths.map(p => normPath(join(p)).toLowerCase())
    const dirs = new Set(
      paths.map(path => getDirs(path)).reduce((a, b) => a.concat(b))
    )
    reservations.set(fn, {dirs, paths})
    paths.forEach(path => {
      const q = queues.get(path)
      if (!q)
        queues.set(path, [fn])
      else
        q.push(fn)
    })
    dirs.forEach(dir => {
      const q = queues.get(dir)
      if (!q)
        queues.set(dir, [new Set([fn])])
      else if (q[q.length-1] instanceof Set)
        q[q.length-1].add(fn)
      else
        q.push(new Set([fn]))
    })

    return run(fn)
  }

  return { check, reserve }
}

module.exports = () => {
  // path => [function or Set]
  // A Set object means a directory reservation
  // A fn is a direct reservation on that path
  const queues = new Map()

  // fn => {paths:[path,...], dirs:[path, ...]}
  const reservations = new Map()

  // return a set of parent dirs for a given path
  // '/a/b/c/d' -> ['/', '/a', '/a/b', '/a/b/c', '/a/b/c/d']
  const getDirs = path => {
    const dirs = path.split('/').slice(0, -1).reduce((set, path) => {
      if (set.length)
        path = normPath(join(set[set.length - 1], path))
      set.push(path || '/')
      return set
    }, [])
    return dirs
  }

  // functions currently running
  const running = new Set()

  // return the queues for each path the function cares about
  // fn => {paths, dirs}
  const getQueues = fn => {
    const res = reservations.get(fn)
    /* istanbul ignore if - unpossible */
    if (!res)
      throw new Error('function does not have any path reservations')
    return {
      paths: res.paths.map(path => queues.get(path)),
      dirs: [...res.dirs].map(path => queues.get(path)),
    }
  }

  // check if fn is first in line for all its paths, and is
  // included in the first set for all its dir queues
  const check = fn => {
    const {paths, dirs} = getQueues(fn)
    return paths.every(q => q[0] === fn) &&
      dirs.every(q => q[0] instanceof Set && q[0].has(fn))
  }

  // run the function if it's first in line and not already running
  const run = fn => {
    if (running.has(fn) || !check(fn))
      return false
    running.add(fn)
    fn(() => clear(fn))
    return true
  }

  const clear = fn => {
    if (!running.has(fn))
      return false

    const { paths, dirs } = reservations.get(fn)
    const next = new Set()

    paths.forEach(path => {
      const q = queues.get(path)
      assert.equal(q[0], fn)
      if (q.length === 1)
        queues.delete(path)
      else {
        q.shift()
        if (typeof q[0] === 'function')
          next.add(q[0])
        else
          q[0].forEach(fn => next.add(fn))
      }
    })

    dirs.forEach(dir => {
      const q = queues.get(dir)
      assert(q[0] instanceof Set)
      if (q[0].size === 1 && q.length === 1) {
        queues.delete(dir)
      } else if (q[0].size === 1) {
        q.shift()

        // must be a function or else the Set would've been reused
        next.add(q[0])
      } else
        q[0].delete(fn)
    })
    running.delete(fn)

    next.forEach(fn => run(fn))
    return true
  }

  const reserve = (paths, fn) => {
    // collide on matches across case and unicode normalization
    // On windows, thanks to the magic of 8.3 shortnames, it is fundamentally
    // impossible to determine whether two paths refer to the same thing on
    // disk, without asking the kernel for a shortname.
    // So, we just pretend that every path matches every other path here,
    // effectively removing all parallelization on windows.
    paths = isWindows ? ['win32 parallelization disabled'] : paths.map(p => {
      return stripSlashes(normPath(join(p)))
        .normalize('NFKD')
        .toLowerCase()
    })

    const dirs = new Set(
      paths.map(path => getDirs(path)).reduce((a, b) => a.concat(b))
    )
    reservations.set(fn, {dirs, paths})
    paths.forEach(path => {
      const q = queues.get(path)
      if (!q)
        queues.set(path, [fn])
      else
        q.push(fn)
    })
    dirs.forEach(dir => {
      const q = queues.get(dir)
      if (!q)
        queues.set(dir, [new Set([fn])])
      else if (q[q.length-1] instanceof Set)
        q[q.length-1].add(fn)
      else
        q.push(new Set([fn]))
    })

    return run(fn)
  }

  return { check, reserve }
}

const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform
const isWindows = platform === 'win32'

--- a/lib/path-reservations.js
+++ b/lib/path-reservations.js
@@ -8,10 +8,16 @@
   const reservations = new Map()

   // return a set of parent dirs for a given path
-  const getDirs = path =>
-    path.split('/').slice(0, -1).reduce((set, path) =>
-      set.length ? set.concat(normPath(join(set[set.length - 1], path)))
-      : [path], [])
+  // '/a/b/c/d' -> ['/', '/a', '/a/b', '/a/b/c', '/a/b/c/d']
+  const getDirs = path => {
+    const dirs = path.split('/').slice(0, -1).reduce((set, path) => {
+      if (set.length)
+        path = normPath(join(set[set.length - 1], path))
+      set.push(path || '/')
+      return set
+    }, [])
+    return dirs
+  }

   // functions currently running
   const running = new Set()
@@ -87,7 +93,18 @@
   }

   const reserve = (paths, fn) => {
-    paths = paths.map(p => normPath(join(p)).toLowerCase())
+    // collide on matches across case and unicode normalization
+    // On windows, thanks to the magic of 8.3 shortnames, it is fundamentally
+    // impossible to determine whether two paths refer to the same thing on
+    // disk, without asking the kernel for a shortname.
+    // So, we just pretend that every path matches every other path here,
+    // effectively removing all parallelization on windows.
+    paths = isWindows ? ['win32 parallelization disabled'] : paths.map(p => {
+      return stripSlashes(normPath(join(p)))
+        .normalize('NFKD')
+        .toLowerCase()
+    })
+
     const dirs = new Set(
       paths.map(path => getDirs(path)).reduce((a, b) => a.concat(b))
     )

--- /dev/null
+++ b/lib/path-reservations.js
@@ -0,0 +1,2 @@
+const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform
+const isWindows = platform === 'win32'

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

cd /workspace/node-tar
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
node test/path-reservations.js

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

cd /workspace/node-tar
git apply --whitespace=nowarn /workspace/fix.patch
node test/path-reservations.js


The package handlebars before 4.7.7 are vulnerable to Remote Code Execution (RCE) when selecting certain compiling options to compile templates coming from an untrusted source.

    strict: function(obj, name, loc) {
      if (!obj || !(name in obj)) {
        throw new Exception('"' + name + '" not defined in ' + obj, {
          loc: loc
        });
      }
      return obj[name];
    },

    strict: function(obj, name, loc) {
      if (!obj || !(name in obj)) {
        throw new Exception('"' + name + '" not defined in ' + obj, {
          loc: loc
        });
      }
      return container.lookupProperty(obj, name);
    },

--- a/lib/handlebars/runtime.js
+++ b/lib/handlebars/runtime.js
@@ -4,5 +4,5 @@
           loc: loc
         });
       }
-      return obj[name];
+      return container.lookupProperty(obj, name);
     },


CryptPad is a collaboration suite. Prior to version 2025.3.0, enforcement of Two-Factor Authentication (2FA) in CryptPad can be trivially bypassed, due to weak implementation of access controls. An attacker that compromises a user's credentials can gain access to the victim's account, even if the victim has 2FA set up. This is due to 2FA not being enforced if the path parameter is not 44 characters long, which can be bypassed by simply URL encoding a single character in the path. This issue has been patched in version 2025.3.0.

Improper Authentication

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

app.use('/block/', function (req, res, next) {
    var parsed = Path.parse(req.url);
    var name = parsed.name;
    // block access control only applies to files
    // identified by base64-encoded public keys
    // skip everything else, ie. /block/placeholder.txt
    if (typeof(name) !== 'string' || name.length !== 44) {
        return void res.status(404).json({
            error: "INVALID_ID",
        });
    }

    var authorization = req.headers.authorization;

    var mfa_params, sso_params;
    nThen(function (w) {
        // First, check whether the block id in question has any MFA settings stored
        MFA.read(Env, name, w(function (err, content) {
            // ENOENT means there are no settings configured
            // it could be a 404 or an existing block without MFA protection
            // in either case you can abort and fall through
            // allowing the static webserver to handle either case
            if (err && err.code === 'ENOENT') {
                return;
            }

            // we're not expecting other errors. the sensible thing is to fail
            // closed - meaning assume some protection is in place but that
            // the settings couldn't be loaded for some reason. block access
            // to the resource, logging for the admin and responding to the client
            // with a vague error code
            if (err) {
                Log.error('GET_BLOCK_METADATA', err);
                return void res.status(500).json({
                    code: 500,
                    error: "UNEXPECTED_ERROR",
                });
            }

            // Otherwise, some settings were loaded correctly.
            // We're expecting stringified JSON, so try to parse it.
            // Log and respond with an error again if this fails.
            // If it parses successfully then fall through to the next block.
            try {
                mfa_params = JSON.parse(content);
            } catch (err2) {
                w.abort();
                Log.error("INVALID_BLOCK_METADATA", err2);
                return res.status(500).json({
                    code: 500,
                    error: "UNEXPECTED_ERROR",
                });
            }
        }));

        // Same for SSO settings
        if (!SSOUtils) { return; }
        SSOUtils.readBlock(Env, name, w(function (err, content) {
            if (err && (err.code === 'ENOENT' || err === 'ENOENT')) {
                return;
            }
            if (err) {
                Log.error('GET_BLOCK_METADATA', err);
                return void res.status(500).json({
                    code: 500,
                    error: "UNEXPECTED_ERROR",
                });
            }
            sso_params = content;
        }));
    }).nThen(function (w) {
        if (!mfa_params && !sso_params) {
            w.abort();
            next();
        }
    }).nThen(function (w) {
        // We should only be able to reach this logic
        // if we successfully loaded and parsed some JSON
        // representing the user's MFA and/or SSO settings.

        // Failures at this point relate to insufficient or incorrect authorization.
        // This function standardizes how we reject such requests.

        // So far the only additional factor which is supported is TOTP.
        // We specify what the method is to allow for future alternatives
        // and inform the client so they can determine how to respond
        // "401" means "Unauthorized"
        var no = function () {
            w.abort();
            res.status(401).json({
                sso: Boolean(sso_params),
                method: mfa_params && mfa_params.method,
                code: 401
            });
        };

        // if you are here it is because this block is protected by MFA or SSO.
        // they will need to provide a JSON Web Token, so we can reject them outright
        // if one is not present in their authorization header
        if (!authorization) { return void no(); }

        // The authorization header should be of the form
        // "Authorization: Bearer <SessionId>"
        // We can reject the request if it is malformed.
        let token = authorization.replace(/^Bearer\s+/, '').trim();
        if (!token) { return void no(); }

        Sessions.read(Env, name, token, function (err, contentStr) {
            if (err) {
                Log.error('SESSION_READ_ERROR', err);
                return res.status(401).json({
                    sso: Boolean(sso_params),
                    method: mfa_params && mfa_params.method,
                    code: 401,
                });
            }

            let content = Util.tryParse(contentStr);

            if (mfa_params && !content.mfa) { return void no(); }
            if (sso_params && !content.sso) { return void no(); }

            if (content.mfa && content.mfa.exp && (+new Date()) > content.mfa.exp) {
                Log.error("OTP_SESSION_EXPIRED", content.mfa);
                Sessions.delete(Env, name, token, function (err) {
                    if (err) {
                        Log.error('SESSION_DELETE_EXPIRED_ERROR', err);
                        return;
                    }
                    Log.info('SESSION_DELETE_EXPIRED', err);
                });
                return void no();
            }

            if (content.sso && content.sso.exp && (+new Date()) > content.sso.exp) {
                Log.error("SSO_SESSION_EXPIRED", content.sso);
                Sessions.delete(Env, name, token, function (err) {
                    if (err) {
                        Log.error('SSO_SESSION_DELETE_EXPIRED_ERROR', err);
                        return;
                    }
                    Log.info('SSO_SESSION_DELETE_EXPIRED', err);
                });
                return void no();
            }

            // Interpret the existence of a file in that location as the continued
            // validity of the session. Fall through and let the built-in webserver
            // handle the 404 or serving the file.
            next();
        });
    });
});

app.use('/block/', function (req, res, next) {
    var parsed = Path.parse(req.url);
    var name = parsed.name;
    // block access control only applies to files
    // identified by base64-encoded public keys
    // skip everything else, ie. /block/placeholder.txt
    if (/placeholder\.txt(\?.+)?/.test(parsed.base)) {
        return void next();
    }
    if (typeof(name) !== 'string' || name.length !== 44) {
        return void res.status(404).json({
            error: "INVALID_ID",
        });
    }

    var authorization = req.headers.authorization;

    var mfa_params, sso_params;
    nThen(function (w) {
        // First, check whether the block id in question has any MFA settings stored
        MFA.read(Env, name, w(function (err, content) {
            // ENOENT means there are no settings configured
            // it could be a 404 or an existing block without MFA protection
            // in either case you can abort and fall through
            // allowing the static webserver to handle either case
            if (err && err.code === 'ENOENT') {
                return;
            }

            // we're not expecting other errors. the sensible thing is to fail
            // closed - meaning assume some protection is in place but that
            // the settings couldn't be loaded for some reason. block access
            // to the resource, logging for the admin and responding to the client
            // with a vague error code
            if (err) {
                Log.error('GET_BLOCK_METADATA', err);
                return void res.status(500).json({
                    code: 500,
                    error: "UNEXPECTED_ERROR",
                });
            }

            // Otherwise, some settings were loaded correctly.
            // We're expecting stringified JSON, so try to parse it.
            // Log and respond with an error again if this fails.
            // If it parses successfully then fall through to the next block.
            try {
                mfa_params = JSON.parse(content);
            } catch (err2) {
                w.abort();
                Log.error("INVALID_BLOCK_METADATA", err2);
                return res.status(500).json({
                    code: 500,
                    error: "UNEXPECTED_ERROR",
                });
            }
        }));

        // Same for SSO settings
        if (!SSOUtils) { return; }
        SSOUtils.readBlock(Env, name, w(function (err, content) {
            if (err && (err.code === 'ENOENT' || err === 'ENOENT')) {
                return;
            }
            if (err) {
                Log.error('GET_BLOCK_METADATA', err);
                return void res.status(500).json({
                    code: 500,
                    error: "UNEXPECTED_ERROR",
                });
            }
            sso_params = content;
        }));
    }).nThen(function (w) {
        if (!mfa_params && !sso_params) {
            w.abort();
            next();
        }
    }).nThen(function (w) {
        // We should only be able to reach this logic
        // if we successfully loaded and parsed some JSON
        // representing the user's MFA and/or SSO settings.

        // Failures at this point relate to insufficient or incorrect authorization.
        // This function standardizes how we reject such requests.

        // So far the only additional factor which is supported is TOTP.
        // We specify what the method is to allow for future alternatives
        // and inform the client so they can determine how to respond
        // "401" means "Unauthorized"
        var no = function () {
            w.abort();
            res.status(401).json({
                sso: Boolean(sso_params),
                method: mfa_params && mfa_params.method,
                code: 401
            });
        };

        // if you are here it is because this block is protected by MFA or SSO.
        // they will need to provide a JSON Web Token, so we can reject them outright
        // if one is not present in their authorization header
        if (!authorization) { return void no(); }

        // The authorization header should be of the form
        // "Authorization: Bearer <SessionId>"
        // We can reject the request if it is malformed.
        let token = authorization.replace(/^Bearer\s+/, '').trim();
        if (!token) { return void no(); }

        Sessions.read(Env, name, token, function (err, contentStr) {
            if (err) {
                Log.error('SESSION_READ_ERROR', err);
                return res.status(401).json({
                    sso: Boolean(sso_params),
                    method: mfa_params && mfa_params.method,
                    code: 401,
                });
            }

            let content = Util.tryParse(contentStr);

            if (mfa_params && !content.mfa) { return void no(); }
            if (sso_params && !content.sso) { return void no(); }

            if (content.mfa && content.mfa.exp && (+new Date()) > content.mfa.exp) {
                Log.error("OTP_SESSION_EXPIRED", content.mfa);
                Sessions.delete(Env, name, token, function (err) {
                    if (err) {
                        Log.error('SESSION_DELETE_EXPIRED_ERROR', err);
                        return;
                    }
                    Log.info('SESSION_DELETE_EXPIRED', err);
                });
                return void no();
            }

            if (content.sso && content.sso.exp && (+new Date()) > content.sso.exp) {
                Log.error("SSO_SESSION_EXPIRED", content.sso);
                Sessions.delete(Env, name, token, function (err) {
                    if (err) {
                        Log.error('SSO_SESSION_DELETE_EXPIRED_ERROR', err);
                        return;
                    }
                    Log.info('SSO_SESSION_DELETE_EXPIRED', err);
                });
                return void no();
            }

            // Interpret the existence of a file in that location as the continued
            // validity of the session. Fall through and let the built-in webserver
            // handle the 404 or serving the file.
            next();
        });
    });
});

--- a/lib/http-worker.js
+++ b/lib/http-worker.js
@@ -4,6 +4,9 @@
     // block access control only applies to files
     // identified by base64-encoded public keys
     // skip everything else, ie. /block/placeholder.txt
+    if (/placeholder\.txt(\?.+)?/.test(parsed.base)) {
+        return void next();
+    }
     if (typeof(name) !== 'string' || name.length !== 44) {
         return void res.status(404).json({
             error: "INVALID_ID",


An issue was discovered in function sync_tree in hetero_decision_tree_guest.py in WeBank FATE (Federated AI Technology Enabler) 0.1 through 1.4.2 allows attackers to read sensitive information during the training process of machine learning joint modeling.

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 sync_tree(self):
        LOGGER.info("sync tree to host")

        self.transfer_inst.tree.remote(self.tree_,
                                       role=consts.HOST,
                                       idx=-1)
        """
        federation.remote(obj=self.tree_,
                          name=self.transfer_inst.tree.name,
                          tag=self.transfer_inst.generate_transferid(self.transfer_inst.tree),
                          role=consts.HOST,
                          idx=-1)
        """

    def sync_tree(self):
        LOGGER.info("sync tree to host")

        tree_nodes = self.remove_sensitive_info()
        self.transfer_inst.tree.remote(tree_nodes,
                                       role=consts.HOST,
                                       idx=-1)
        """
        federation.remote(obj=self.tree_,
                          name=self.transfer_inst.tree.name,
                          tag=self.transfer_inst.generate_transferid(self.transfer_inst.tree),
                          role=consts.HOST,
                          idx=-1)
        """

        return model_param

    def set_model_param(self, model_param):
        self.tree_ = []
        for node_param in model_param.tree_:
            _node = Node(id=node_param.id,
                         sitename=node_param.sitename,
                         fid=node_param.fid,
                         bid=node_param.bid,
                         weight=node_param.weight,
                         is_leaf=node_param.is_leaf,

--- a/federatedml/tree/hetero/hetero_decision_tree_guest.py
+++ b/federatedml/tree/hetero/hetero_decision_tree_guest.py
@@ -1,7 +1,8 @@
     def sync_tree(self):
         LOGGER.info("sync tree to host")

-        self.transfer_inst.tree.remote(self.tree_,
+        tree_nodes = self.remove_sensitive_info()
+        self.transfer_inst.tree.remote(tree_nodes,
                                        role=consts.HOST,
                                        idx=-1)
         """

--- /dev/null
+++ b/federatedml/tree/hetero/hetero_decision_tree_guest.py
@@ -0,0 +1,11 @@
+        return model_param
+
+    def set_model_param(self, model_param):
+        self.tree_ = []
+        for node_param in model_param.tree_:
+            _node = Node(id=node_param.id,
+                         sitename=node_param.sitename,
+                         fid=node_param.fid,
+                         bid=node_param.bid,
+                         weight=node_param.weight,
+                         is_leaf=node_param.is_leaf,

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

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

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

cd /workspace/FATE
git apply --whitespace=nowarn /workspace/fix.patch
/workspace/PoC_env/CVE-2020-25459/bin/python -m pytest federatedml/tree/test/ -v


Cilium is a networking, observability, and security solution with an eBPF-based dataplane. Prior to versions 1.14.14 and 1.15.8, a race condition in the Cilium agent can cause the agent to ignore labels that should be applied to a node. This could in turn cause CiliumClusterwideNetworkPolicies intended for nodes with the ignored label to not apply, leading to policy bypass. This issue has been patched in Cilium v1.14.14 and v1.15.8 As the underlying issue depends on a race condition, users unable to upgrade can restart the Cilium agent on affected nodes until the affected policies are confirmed to be working as expected.

Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')

The product contains a concurrent code sequence that requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence operating concurrently.

func (mgr *endpointManager) startNodeLabelsObserver(old map[string]string) {
	mgr.localNodeStore.Observe(context.Background(), func(ln node.LocalNode) {
		if maps.Equal(old, ln.Labels) {
			return
		}

		mgr.updateHostEndpointLabels(old, ln.Labels)
		old = ln.Labels
	}, func(error) { /* Executed only when we are shutting down */ })
}

func (mgr *endpointManager) updateHostEndpointLabels(oldNodeLabels, newNodeLabels map[string]string) {
	nodeEP := mgr.GetHostEndpoint()
	if nodeEP == nil {
		log.Error("Host endpoint not found")
		return
	}

	err := nodeEP.UpdateLabelsFrom(oldNodeLabels, newNodeLabels, labels.LabelSourceK8s)
	if err != nil {
		// An error can only occur if either the endpoint is terminating, or the
		// old labels are not found. Both are impossible, hence there's no point
		// in retrying.
		log.WithError(err).Error("Unable to update host endpoint labels")
		return
	}
}

func (mgr *endpointManager) startNodeLabelsObserver(old map[string]string) {
	mgr.localNodeStore.Observe(context.Background(), func(ln node.LocalNode) {
		if maps.Equal(old, ln.Labels) {
			return
		}

		if mgr.updateHostEndpointLabels(old, ln.Labels) {
			// Endpoint's label update logic rejects a request if any of the old labels are
			// not present in the endpoint manager's state. So, overwrite old labels only if
			// the update is successful to avoid node labels being outdated indefinitely (GH-29649).
			old = ln.Labels
		}

	}, func(error) { /* Executed only when we are shutting down */ })
}

func (mgr *endpointManager) updateHostEndpointLabels(oldNodeLabels, newNodeLabels map[string]string) bool {
	nodeEP := mgr.GetHostEndpoint()
	if nodeEP == nil {
		log.Error("Host endpoint not found")
		return false
	}

	if err := nodeEP.UpdateLabelsFrom(oldNodeLabels, newNodeLabels, labels.LabelSourceK8s); err != nil {
		// An error can only occur if either the endpoint is terminating, or the
		// old labels are not found. Both are impossible, hence there's no point
		// in retrying.
		log.WithError(err).Error("Unable to update host endpoint labels")
		return false
	}
	return true
}

--- a/pkg/endpointmanager/host.go
+++ b/pkg/endpointmanager/host.go
@@ -4,7 +4,12 @@
 			return
 		}

-		mgr.updateHostEndpointLabels(old, ln.Labels)
-		old = ln.Labels
+		if mgr.updateHostEndpointLabels(old, ln.Labels) {
+			// Endpoint's label update logic rejects a request if any of the old labels are
+			// not present in the endpoint manager's state. So, overwrite old labels only if
+			// the update is successful to avoid node labels being outdated indefinitely (GH-29649).
+			old = ln.Labels
+		}
+
 	}, func(error) { /* Executed only when we are shutting down */ })
 }

--- a/pkg/endpointmanager/host.go
+++ b/pkg/endpointmanager/host.go
@@ -1,16 +1,16 @@
-func (mgr *endpointManager) updateHostEndpointLabels(oldNodeLabels, newNodeLabels map[string]string) {
+func (mgr *endpointManager) updateHostEndpointLabels(oldNodeLabels, newNodeLabels map[string]string) bool {
 	nodeEP := mgr.GetHostEndpoint()
 	if nodeEP == nil {
 		log.Error("Host endpoint not found")
-		return
+		return false
 	}

-	err := nodeEP.UpdateLabelsFrom(oldNodeLabels, newNodeLabels, labels.LabelSourceK8s)
-	if err != nil {
+	if err := nodeEP.UpdateLabelsFrom(oldNodeLabels, newNodeLabels, labels.LabelSourceK8s); err != nil {
 		// An error can only occur if either the endpoint is terminating, or the
 		// old labels are not found. Both are impossible, hence there's no point
 		// in retrying.
 		log.WithError(err).Error("Unable to update host endpoint labels")
-		return
+		return false
 	}
+	return true
 }


The package @isomorphic-git/cors-proxy before 2.7.1 are vulnerable to Server-side Request Forgery (SSRF) due to missing sanitization and validation of the redirection action in middleware.js.

  function middleware (req, res) {
    let u = url.parse(req.url, true)

    let headers = {}
    for (let h of allowHeaders) {
      if (req.headers[h]) {
        headers[h] = req.headers[h]
      }
    }

    // GitHub uses user-agent sniffing for git/* and changes its behavior which is frustrating
    if (!headers['user-agent'] || !headers['user-agent'].startsWith('git/')) {
      headers['user-agent'] = 'git/@isomorphic-git/cors-proxy'
    }

    let p = u.path
    let parts = p.match(/\/([^\/]*)\/(.*)/)
    let pathdomain = parts[1]
    let remainingpath = parts[2]
    let protocol = insecure_origins.includes(pathdomain) ? 'http' : 'https'

    fetch(
      `${protocol}://${pathdomain}/${remainingpath}`,
      {
        method: req.method,
        headers,
        body: (req.method !== 'GET' && req.method !== 'HEAD') ? req : undefined
      }
    ).then(f => {
      res.statusCode = f.status
      for (let h of exposeHeaders) {
        if (h === 'content-length') continue
        if (f.headers.has(h)) {
          res.setHeader(h, f.headers.get(h))
        }
      }
      if (f.redirected) {
        res.setHeader('x-redirected-url', f.url)
      }
      f.body.pipe(res)
    })
  }
  const cors = microCors({
    allowHeaders,
    exposeHeaders,
    allowMethods,
    allowCredentials: false,
    origin
  })
  return filter(predicate, cors(compose(sendCorsOK, authorization, middleware)))
}

const exposeHeaders = [
  'accept-ranges',
  'age',
  'cache-control',
  'content-length',
  'content-language',
  'content-type',
  'date',
  'etag',
  'expires',
  'last-modified',
  'pragma',
  'server',
  'transfer-encoding',
  'vary',
  'x-github-request-id',
  'x-redirected-url',
]

  function middleware (req, res) {
    let u = url.parse(req.url, true)

    let headers = {}
    for (let h of allowHeaders) {
      if (req.headers[h]) {
        headers[h] = req.headers[h]
      }
    }

    // GitHub uses user-agent sniffing for git/* and changes its behavior which is frustrating
    if (!headers['user-agent'] || !headers['user-agent'].startsWith('git/')) {
      headers['user-agent'] = 'git/@isomorphic-git/cors-proxy'
    }

    let p = u.path
    let parts = p.match(/\/([^\/]*)\/(.*)/)
    let pathdomain = parts[1]
    let remainingpath = parts[2]
    let protocol = insecure_origins.includes(pathdomain) ? 'http' : 'https'

    fetch(
      `${protocol}://${pathdomain}/${remainingpath}`,
      {
        method: req.method,
        redirect: 'manual',
        headers,
        body: (req.method !== 'GET' && req.method !== 'HEAD') ? req : undefined
      }
    ).then(f => {
      if (f.headers.has('location')) {
        // Modify the location so the client continues to use the proxy
        let newUrl = f.headers.get('location').replace(/^https?:\//, '')
        f.headers.set('location', newUrl)
      }
      res.statusCode = f.status
      for (let h of exposeHeaders) {
        if (h === 'content-length') continue
        if (f.headers.has(h)) {
          res.setHeader(h, f.headers.get(h))
        }
      }
      if (f.redirected) {
        res.setHeader('x-redirected-url', f.url)
      }
      f.body.pipe(res)
    })
  }
  const cors = microCors({
    allowHeaders,
    exposeHeaders,
    allowMethods,
    allowCredentials: false,
    origin
  })
  return filter(predicate, cors(compose(sendCorsOK, authorization, middleware)))
}

const exposeHeaders = [
  'accept-ranges',
  'age',
  'cache-control',
  'content-length',
  'content-language',
  'content-type',
  'date',
  'etag',
  'expires',
  'last-modified',
  'location',
  'pragma',
  'server',
  'transfer-encoding',
  'vary',
  'x-github-request-id',
  'x-redirected-url',
]

--- a/middleware.js
+++ b/middleware.js
@@ -24,10 +24,16 @@
       `${protocol}://${pathdomain}/${remainingpath}`,
       {
         method: req.method,
+        redirect: 'manual',
         headers,
         body: (req.method !== 'GET' && req.method !== 'HEAD') ? req : undefined
       }
     ).then(f => {
+      if (f.headers.has('location')) {
+        // Modify the location so the client continues to use the proxy
+        let newUrl = f.headers.get('location').replace(/^https?:\//, '')
+        f.headers.set('location', newUrl)
+      }
       res.statusCode = f.status
       for (let h of exposeHeaders) {
         if (h === 'content-length') continue

--- a/middleware.js
+++ b/middleware.js
@@ -9,6 +9,7 @@
   'etag',
   'expires',
   'last-modified',
+  'location',
   'pragma',
   'server',
   'transfer-encoding',


Zope is an open-source web application server. In Zope versions prior to 4.6 and 5.2, users can access untrusted modules indirectly through Python modules that are available for direct use. By default, only users with the Manager role can add or edit Zope Page Templates through the web, but sites that allow untrusted users to add/edit Zope Page Templates through the web are at risk from this vulnerability. The problem has been fixed in Zope 5.2 and 4.6. As a workaround, a site administrator can restrict adding/editing Zope Page Templates through the web using the standard Zope user/role permission mechanisms. Untrusted users should not be assigned the Zope Manager role and adding/editing Zope Page Templates through the web should be restricted to trusted users only.

def boboAwareZopeTraverse(object, path_items, econtext):
    """Traverses a sequence of names, first trying attributes then items.

    This uses zope.traversing path traversal where possible and interacts
    correctly with objects providing OFS.interface.ITraversable when
    necessary (bobo-awareness).
    """
    request = getattr(econtext, 'request', None)
    path_items = list(path_items)
    path_items.reverse()

    while path_items:
        name = path_items.pop()
        if OFS.interfaces.ITraversable.providedBy(object):
            object = object.restrictedTraverse(name)
        else:
            object = traversePathElement(object, name, path_items,
                                         request=request)
    return object

    def traverse(cls, base, request, path_items):
        """See ``zope.app.pagetemplate.engine``."""

        path_items = list(path_items)
        path_items.reverse()

        while path_items:
            name = path_items.pop()
            if ITraversable.providedBy(base):
                base = getattr(base, cls.traverseMethod)(name)
            else:
                base = traversePathElement(base, name, path_items,
                                           request=request)

        return base

def boboAwareZopeTraverse(object, path_items, econtext):
    """Traverses a sequence of names, first trying attributes then items.

    This uses zope.traversing path traversal where possible and interacts
    correctly with objects providing OFS.interface.ITraversable when
    necessary (bobo-awareness).
    """
    request = getattr(econtext, 'request', None)
    path_items = list(path_items)
    path_items.reverse()

    while path_items:
        name = path_items.pop()

        if name == '_':
            warnings.warn('Traversing to the name `_` is deprecated '
                          'and will be removed in Zope 6.',
                          DeprecationWarning)
        elif name.startswith('_'):
            raise NotFound(name)

        if OFS.interfaces.ITraversable.providedBy(object):
            object = object.restrictedTraverse(name)
        else:
            object = traversePathElement(object, name, path_items,
                                         request=request)
    return object

    def traverse(cls, base, request, path_items):
        """See ``zope.app.pagetemplate.engine``."""

        path_items = list(path_items)
        path_items.reverse()

        while path_items:
            name = path_items.pop()

            if name == '_':
                warnings.warn('Traversing to the name `_` is deprecated '
                              'and will be removed in Zope 6.',
                              DeprecationWarning)
            elif name.startswith('_'):
                raise NotFound(name)

            if ITraversable.providedBy(base):
                base = getattr(base, cls.traverse_method)(name)
            else:
                base = traversePathElement(base, name, path_items,
                                           request=request)

        return base

--- a/src/Products/PageTemplates/Expressions.py
+++ b/src/Products/PageTemplates/Expressions.py
@@ -11,6 +11,14 @@

     while path_items:
         name = path_items.pop()
+
+        if name == '_':
+            warnings.warn('Traversing to the name `_` is deprecated '
+                          'and will be removed in Zope 6.',
+                          DeprecationWarning)
+        elif name.startswith('_'):
+            raise NotFound(name)
+
         if OFS.interfaces.ITraversable.providedBy(object):
             object = object.restrictedTraverse(name)
         else:

--- a/src/Products/PageTemplates/expression.py
+++ b/src/Products/PageTemplates/expression.py
@@ -6,8 +6,16 @@

         while path_items:
             name = path_items.pop()
+
+            if name == '_':
+                warnings.warn('Traversing to the name `_` is deprecated '
+                              'and will be removed in Zope 6.',
+                              DeprecationWarning)
+            elif name.startswith('_'):
+                raise NotFound(name)
+
             if ITraversable.providedBy(base):
-                base = getattr(base, cls.traverseMethod)(name)
+                base = getattr(base, cls.traverse_method)(name)
             else:
                 base = traversePathElement(base, name, path_items,
                                            request=request)