CVE-2022-21682: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Flatpak is a Linux application sandboxing and distribution framework. A path traversal vulnerability affects versions of Flatpak prior to 1.12.3 and 1.10.6. flatpak-builder applies `finish-args` last in the build. At this point the build directory will have the full access that is specified in the manifest, so running `flatpak build` against it will gain those permissions. Normally this will not be done, so this is not problem. However, if `--mirror-screenshots-url` is specified, then flatpak-builder will launch `flatpak build --nofilesystem=host appstream-utils mirror-screenshots` after finalization, which can lead to issues even with the `--nofilesystem=host` protection. In normal use, the only issue is that these empty directories can be created wherever the user has write permissions. However, a malicious application could replace the `appstream-util` binary and potentially do something more hostile. This has been resolved in Flatpak 1.12.3 and 1.10.6 by changing the behaviour of `--nofilesystem=home` and `--nofilesystem=host`.
CWE description: 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.
CVSS v3 base score: 6.5
Published: 2022-01-13T21:15Z
Repository: https://github.com/flatpak/flatpak
Language: C
Files changed: common/flatpak-context.c
Commit date: 2022-01-12 19:48:34 +0100
Commit message: Make --nofilesystem=host/home remove access to subdirs of those

Previously --nofilesystem=host only removed specifically access to the
`host` permissions, and not necessarily other filesystems (like `home`
or `/some/path`). This isn't very useful to limit access because you
don't know what other filesystems the app may have access too.

We change this to mean that `--nofilesystem=host` removes *all* filesystem
access from the parent layer, and `--nofilesystem=home` removes all
file access to the homedir and paths inside it.

The available layers are, in order:

 * app permissions
 * overrides
 * commandline args

This allows you to start from scratch with the filesystem permissions
in the overrides or the commandline. This is a small change in
behaviour, but not a lot of things use --nofilesystem, and the ones
that do probably expects this behaviour.

Diff: diff --git a/common/flatpak-context.c b/common/flatpak-context.c
index dd230d26fc..ce491fa05e 100644
--- a/common/flatpak-context.c
+++ b/common/flatpak-context.c
@@ -850,10 +850,35 @@ flatpak_context_parse_filesystem (const char             *filesystem_and_mode,
   g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED,
                _("Unknown filesystem location %s, valid locations are: host, host-os, host-etc, home, xdg-*[/…], ~/dir, /dir"), filesystem);
   return FALSE;
 }
 
+/* Note: This only works with valid keys, i.e. they passed flatpak_context_parse_filesystem */
+static gboolean
+flatpak_filesystem_key_in_home (const char *filesystem)
+{
+  /* "home" is definitely in home */
+  if (strcmp (filesystem, "home") == 0)
+    return TRUE;
+
+  /* All the other special fs:es are non-home.
+   * Note: This considers absolute paths that are in the homedir as non-home.
+   */
+  if (g_strv_contains (flatpak_context_special_filesystems, filesystem) ||
+      g_str_has_prefix (filesystem, "/"))
+    return FALSE;
+
+  /* Files in xdg-run are not in home */
+  if (g_str_has_prefix (filesystem, "xdg-run"))
+    return FALSE;
+
+  /* All remaining keys (~/, xdg-data, etc) are considered in home,
+   * Note: technically $XDG_HOME_DATA could point outside the homedir, but we ignore that.
+   */
+  return TRUE;
+}
+
 static void
 flatpak_context_take_filesystem (FlatpakContext        *context,
                                  char                  *fs,
                                  FlatpakFilesystemMode  mode)
 {
@@ -864,10 +889,12 @@ void
 flatpak_context_merge (FlatpakContext *context,
                        FlatpakContext *other)
 {
   GHashTableIter iter;
   gpointer key, value;
+  gboolean no_home = FALSE;
+  gboolean no_host = FALSE;
 
   context->shares &= ~other->shares_valid;
   context->shares |= other->shares;
   context->shares_valid |= other->shares_valid;
   context->sockets &= ~other->sockets_valid;
@@ -886,10 +913,45 @@ flatpak_context_merge (FlatpakContext *context,
 
   g_hash_table_iter_init (&iter, other->persistent);
   while (g_hash_table_iter_next (&iter, &key, &value))
     g_hash_table_insert (context->persistent, g_strdup (key), value);
 
+  /* We first handle all negative home and host as they override other
+     keys than themselves from the parent */
+  if (g_hash_table_lookup_extended (other->filesystems,
+                                    "host",
+                                    NULL, &value))
+    {
+      FlatpakFilesystemMode host_mode = GPOINTER_TO_INT (value);
+      if (host_mode == FLATPAK_FILESYSTEM_MODE_NONE)
+        no_host = TRUE;
+    }
+
+  if (g_hash_table_lookup_extended (other->filesystems,
+                                    "home",
+                                    NULL, &value))
+    {
+      FlatpakFilesystemMode home_mode = GPOINTER_TO_INT (value);
+      if (home_mode == FLATPAK_FILESYSTEM_MODE_NONE)
+        no_home = TRUE;
+    }
+
+  if (no_host)
+    {
+      g_hash_table_remove_all (context->filesystems);
+    }
+  else if (no_home)
+    {
+      g_hash_table_iter_init (&iter, context->filesystems);
+      while (g_hash_table_iter_next (&iter, &key, &value))
+        {
+          if (flatpak_filesystem_key_in_home ((const char *)key))
+            g_hash_table_iter_remove (&iter);
+        }
+    }
+
+  /* Then set the new ones, which includes propagating the nohost and nohome ones. */
   g_hash_table_iter_init (&iter, other->filesystems);
   while (g_hash_table_iter_next (&iter, &key, &value))
     g_hash_table_insert (context->filesystems, g_strdup (key), value);
 
   g_hash_table_iter_init (&iter, other->session_bus_policy);

Fixed code: /* Note: This only works with valid keys, i.e. they passed flatpak_context_parse_filesystem */
static gboolean
flatpak_filesystem_key_in_home (const char *filesystem)
{
  /* "home" is definitely in home */
  if (strcmp (filesystem, "home") == 0)
    return TRUE;

  /* All the other special fs:es are non-home.
   * Note: This considers absolute paths that are in the homedir as non-home.
   */
  if (g_strv_contains (flatpak_context_special_filesystems, filesystem) ||
      g_str_has_prefix (filesystem, "/"))
    return FALSE;

  /* Files in xdg-run are not in home */
  if (g_str_has_prefix (filesystem, "xdg-run"))
    return FALSE;

  /* All remaining keys (~/, xdg-data, etc) are considered in home,
   * Note: technically $XDG_HOME_DATA could point outside the homedir, but we ignore that.
   */
  return TRUE;
}

  gboolean no_home = FALSE;
  gboolean no_host = FALSE;
  /* We first handle all negative home and host as they override other
     keys than themselves from the parent */
  if (g_hash_table_lookup_extended (other->filesystems,
                                    "host",
                                    NULL, &value))
    {
      FlatpakFilesystemMode host_mode = GPOINTER_TO_INT (value);
      if (host_mode == FLATPAK_FILESYSTEM_MODE_NONE)
        no_host = TRUE;
    }

  if (g_hash_table_lookup_extended (other->filesystems,
                                    "home",
                                    NULL, &value))
    {
      FlatpakFilesystemMode home_mode = GPOINTER_TO_INT (value);
      if (home_mode == FLATPAK_FILESYSTEM_MODE_NONE)
        no_home = TRUE;
    }

  if (no_host)
    {
      g_hash_table_remove_all (context->filesystems);
    }
  else if (no_home)
    {
      g_hash_table_iter_init (&iter, context->filesystems);
      while (g_hash_table_iter_next (&iter, &key, &value))
        {
          if (flatpak_filesystem_key_in_home ((const char *)key))
            g_hash_table_iter_remove (&iter);
        }
    }

  /* Then set the new ones, which includes propagating the nohost and nohome ones. */