From bd380777d68816b55da85a42d4cdf7fb262b4ba2 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: Make the output of 'fscrypt status' unambiguous Following the example of /proc/self/mountinfo, replace the space, newline, tab, and backslash characters with octal escape sequences so that the output can be parsed unambiguously. --- cmd/fscrypt/status.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'cmd/fscrypt') diff --git a/cmd/fscrypt/status.go b/cmd/fscrypt/status.go index 255bb2b..d10dfd8 100644 --- a/cmd/fscrypt/status.go +++ b/cmd/fscrypt/status.go @@ -114,8 +114,11 @@ func writeGlobalStatus(w io.Writer) error { continue } - fmt.Fprintf(t, "%s\t%s\t%s\t%s\t%s\n", mount.Path, mount.Device, - mount.FilesystemType, supportString, yesNoString(usingFscrypt)) + fmt.Fprintf(t, "%s\t%s\t%s\t%s\t%s\n", + filesystem.EscapeString(mount.Path), + filesystem.EscapeString(mount.Device), + filesystem.EscapeString(mount.FilesystemType), + supportString, yesNoString(usingFscrypt)) if supportErr == nil { supportCount++ -- cgit v1.2.3 From fa1a1fdbdea65829ce24a6b6f86ce2961e465b02 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: bash_completion: fix command injection and incorrect completions Mountpoint paths might be untrusted arbitrary strings; the fscrypt bash completion script might need to complete to such strings. Unfortunately, the design of bash completion places some major footguns in the way of doing this correctly and securely: - "compgen -W" expands anything passed to it, so the argument to -W must be single-quoted to avoid an extra level of expansion. - The backslashes needed to escape meta-characters in the completed text aren't added automatically; they must be explicitly added. Note that the completion script for 'umount' used to have these same bugs (https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=892179, https://github.com/util-linux/util-linux/issues/539). Fix these bugs in roughly the same way that 'umount' fixed them. --- cmd/fscrypt/fscrypt_bash_completion | 78 +++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 16 deletions(-) (limited to 'cmd/fscrypt') diff --git a/cmd/fscrypt/fscrypt_bash_completion b/cmd/fscrypt/fscrypt_bash_completion index 00ee490..110d2d4 100644 --- a/cmd/fscrypt/fscrypt_bash_completion +++ b/cmd/fscrypt/fscrypt_bash_completion @@ -15,25 +15,71 @@ # License for the specific language governing permissions and limitations under # the License. - -# Prefer completion script style COMPREPLY=($(...)) assignment. +# +# bash completion scripts require exercising some unusual shell script +# features/quirks, so we have to disable some shellcheck warnings: +# +# Disable SC2016 ("Expressions don't expand in single quotes, use double quotes +# for that") because the 'compgen' built-in expands the argument passed to -W, +# so that argument *must* be single-quoted to avoid command injection. +# shellcheck disable=SC2016 +# +# Disable SC2034 ("{Variable} appears unused. Verify use (or export if used +# externally)") because of the single quoting mentioned above as well as the +# fact that we have to declare "local" variables used only by a called function +# (_init_completion()) and not by the function itself. +# shellcheck disable=SC2034 +# +# Disable SC2207 ("Prefer mapfile or read -a to split command output (or quote +# to avoid splitting)") because bash completion scripts conventionally use +# COMPREPLY=($(...)) assignments. # shellcheck disable=SC2207 -true # To apply shellcheck directive to all file +# +true # To apply the above shellcheck directives to the entire file -# Output list of possible mount points -_fscrypt_mountpoints() +# Generate the completion list for possible mountpoints. +# +# We need to be super careful here because mountpoints can contain whitespace +# and shell meta-characters. To avoid most problems, we do the following: +# +# 1.) To avoid parsing ambiguities, 'fscrypt status' replaces the space, tab, +# newline, and backslash characters with octal escape sequences -- like +# what /proc/self/mountinfo does. To properly process its output, we need +# to split lines on space only (and not on other whitespace which might +# not be escaped), and unescape these characters. Exception: we don't +# unescape newlines, as we need to reserve newline as the separator for +# the words passed to compgen. (This causes mountpoints containing +# newlines to not be completed correctly, which we have to tolerate.) +# +# 2.) We backslash-escape all shell meta-characters, and single-quote the +# argument passed to compgen -W. Without either step, command injection +# would be possible. Without both steps, completions would be incorrect. +# The list of shell meta-characters used comes from that used by the +# completion script for umount, which has to solve this same problem. +# +_fscrypt_compgen_mountpoints() { - # shellcheck disable=SC2016 - fscrypt status 2>/dev/null | \ - command awk 'substr($0, 1, 1) == "/" && $5 == "Yes" { print $1 }' + local IFS=$'\n' + compgen -W '$(_fscrypt_mountpoints_internal)' -- "${cur}" } +_fscrypt_mountpoints_internal() +{ + fscrypt status 2>/dev/null | command awk -F " " \ + 'substr($0, 1, 1) == "/" && $5 == "Yes" { + gsub(/\\040/, " ", $1) + gsub(/\\011/, "\t", $1) + gsub(/\\134/, "\\", $1) + gsub(/[\]\[(){}<>",:;^&!$=?`|'\''\\ \t\f\n\r\v]/, "\\\\&", $1) + print $1 + }' +} # Complete with all possible mountpoints _fscrypt_complete_mountpoint() { - COMPREPLY=($(compgen -W "$(_fscrypt_mountpoints)" -- "${cur}")) + COMPREPLY=($(_fscrypt_compgen_mountpoints)) } @@ -43,7 +89,6 @@ _fscrypt_complete_mountpoint() _fscrypt_status_section() { local section=${2^^} - # shellcheck disable=SC2016 fscrypt status "$1" 2>/dev/null | \ command awk '/^[[:xdigit:]]{16}/ && section == "'"$section"'" { print $1; next; } { section = $1 }' @@ -57,13 +102,13 @@ _fscrypt_complete_policy_or_protector() if [[ $cur = *:* ]]; then # Complete with IDs of the given mountpoint local mountpoint="${cur%:*}" id="${cur#*:}" + # Note: compgen expands the argument to -W, so it *must* be single-quoted. COMPREPLY=($(compgen \ - -W "$(_fscrypt_status_section "${mountpoint}" "${status_section}")" \ + -W '$(_fscrypt_status_section "${mountpoint}" "${status_section}")' \ -- "${id}")) else # Complete with mountpoints, with colon and without ending space - COMPREPLY=($(compgen -W "$(_fscrypt_mountpoints)" \ - -- "${cur}" | sed s/\$/:/)) + COMPREPLY=($(_fscrypt_compgen_mountpoints | sed s/\$/:/)) compopt -o nospace fi } @@ -72,7 +117,8 @@ _fscrypt_complete_policy_or_protector() # Complete with all arguments of that function _fscrypt_complete_word() { - COMPREPLY=($(compgen -W "$*" -- "${cur}")) + # Note: compgen expands the argument to -W, so it *must* be single-quoted. + COMPREPLY=($(compgen -W '$*' -- "${cur}")) } @@ -82,7 +128,8 @@ _fscrypt_complete_option() local additional_opts=( "$@" ) # Add global options, always correct additional_opts+=( --verbose --quiet --help ) - COMPREPLY=($(compgen -W "${additional_opts[*]}" -- "${cur}")) + # Note: compgen expands the argument to -W, so it *must* be single-quoted. + COMPREPLY=($(compgen -W '${additional_opts[*]}' -- "${cur}")) } @@ -95,7 +142,6 @@ _fscrypt() # # `split` is set by `_init_completion -s`, we must declare it local # even if we don't use it, not to modify the environment. - # shellcheck disable=SC2034 local cur prev words cword split _init_completion -s -n : || return -- cgit v1.2.3 From 6e355131670ad014e45f879475ddf800f0080d41 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: Make 'fscrypt setup' offer a choice of directory modes World-writable directories are not appropriate for some systems, so offer a choice of single-user-writable and world-writable modes, with single-user-writable being the default. Add a new documentation section to help users decide which one to use. --- cmd/fscrypt/commands.go | 6 +++--- cmd/fscrypt/errors.go | 4 ++++ cmd/fscrypt/flags.go | 14 ++++++++++++-- cmd/fscrypt/setup.go | 41 +++++++++++++++++++++++++++++++++++++++-- cmd/fscrypt/status.go | 11 ++++++++++- 5 files changed, 68 insertions(+), 8 deletions(-) (limited to 'cmd/fscrypt') diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index 023c0fa..30aa3a7 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -63,7 +63,7 @@ var Setup = cli.Command{ the README). This may require root privileges.`, mountpointArg, actions.ConfigFileLocation, shortDisplay(timeTargetFlag)), - Flags: []cli.Flag{timeTargetFlag, forceFlag}, + Flags: []cli.Flag{timeTargetFlag, forceFlag, allUsersSetupFlag}, Action: setupAction, } @@ -468,7 +468,7 @@ var Lock = cli.Command{ recoverable by an attacker who compromises system memory. To be fully safe, you must reboot with a power cycle.`, directoryArg, shortDisplay(dropCachesFlag)), - Flags: []cli.Flag{dropCachesFlag, userFlag, allUsersFlag}, + Flags: []cli.Flag{dropCachesFlag, userFlag, allUsersLockFlag}, Action: lockAction, } @@ -502,7 +502,7 @@ func lockAction(c *cli.Context) error { return newExitError(c, ErrDropCachesPerm) } - if err = policy.Deprovision(allUsersFlag.Value); err != nil { + if err = policy.Deprovision(allUsersLockFlag.Value); err != nil { switch err { case keyring.ErrKeyNotPresent: break diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go index bcf5b59..2d76792 100644 --- a/cmd/fscrypt/errors.go +++ b/cmd/fscrypt/errors.go @@ -232,6 +232,10 @@ func getErrorSuggestions(err error) string { } } return "" + case *filesystem.ErrNoCreatePermission: + return `For how to allow users to create fscrypt metadata on a + filesystem, refer to + https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem` case *filesystem.ErrNotSetup: return fmt.Sprintf(`Run "sudo fscrypt setup %s" to use fscrypt on this filesystem.`, e.Mount.Path) diff --git a/cmd/fscrypt/flags.go b/cmd/fscrypt/flags.go index 044b71e..1b41839 100644 --- a/cmd/fscrypt/flags.go +++ b/cmd/fscrypt/flags.go @@ -116,7 +116,8 @@ var ( allFlags = []prettyFlag{helpFlag, versionFlag, verboseFlag, quietFlag, forceFlag, skipUnlockFlag, timeTargetFlag, sourceFlag, nameFlag, keyFileFlag, protectorFlag, - unlockWithFlag, policyFlag, allUsersFlag, noRecoveryFlag} + unlockWithFlag, policyFlag, allUsersLockFlag, allUsersSetupFlag, + noRecoveryFlag} // universalFlags contains flags that should be on every command universalFlags = []cli.Flag{verboseFlag, quietFlag, helpFlag} ) @@ -164,7 +165,7 @@ var ( privileges.`, Default: true, } - allUsersFlag = &boolFlag{ + allUsersLockFlag = &boolFlag{ Name: "all-users", Usage: `Lock the directory no matter which user(s) have unlocked it. Requires root privileges. This flag is only @@ -172,6 +173,15 @@ var ( different from the one you're locking it as. This flag is only implemented for v2 encryption policies.`, } + allUsersSetupFlag = &boolFlag{ + Name: "all-users", + Usage: `When setting up a filesystem for fscrypt, allow users + other than the calling user (typically root) to create + fscrypt policies and protectors on the filesystem. Note + that this will create a world-writable directory, which + users could use to fill up the entire filesystem. Hence, + this option may not be appropriate for some systems.`, + } noRecoveryFlag = &boolFlag{ Name: "no-recovery", Usage: `Don't generate a recovery passphrase.`, diff --git a/cmd/fscrypt/setup.go b/cmd/fscrypt/setup.go index 7b9bebb..1da0d16 100644 --- a/cmd/fscrypt/setup.go +++ b/cmd/fscrypt/setup.go @@ -26,6 +26,7 @@ import ( "os" "github.com/google/fscrypt/actions" + "github.com/google/fscrypt/filesystem" "github.com/google/fscrypt/util" ) @@ -80,11 +81,47 @@ func setupFilesystem(w io.Writer, path string) error { if err != nil { return err } + username := ctx.TargetUser.Username - if err = ctx.Mount.Setup(); err != nil { + err = ctx.Mount.CheckSetup() + if err == nil { + return &filesystem.ErrAlreadySetup{Mount: ctx.Mount} + } + if _, ok := err.(*filesystem.ErrNotSetup); !ok { return err } - fmt.Fprintf(w, "Metadata directories created at %q.\n", ctx.Mount.BaseDir()) + allUsers := allUsersSetupFlag.Value + if !allUsers { + thisFilesystem := "this filesystem" + if ctx.Mount.Path == "/" { + thisFilesystem = "the root filesystem" + } + prompt := fmt.Sprintf(`Allow users other than %s to create +fscrypt metadata on %s? (See +https://github.com/google/fscrypt#setting-up-fscrypt-on-a-filesystem)`, + username, thisFilesystem) + allUsers, err = askQuestion(wrapText(prompt, 0), false) + if err != nil { + return err + } + } + var setupMode filesystem.SetupMode + if allUsers { + setupMode = filesystem.WorldWritable + } else { + setupMode = filesystem.SingleUserWritable + } + if err = ctx.Mount.Setup(setupMode); err != nil { + return err + } + + if allUsers { + fmt.Fprintf(w, "Metadata directories created at %q, writable by everyone.\n", + ctx.Mount.BaseDir()) + } else { + fmt.Fprintf(w, "Metadata directories created at %q, writable by %s only.\n", + ctx.Mount.BaseDir(), username) + } return nil } diff --git a/cmd/fscrypt/status.go b/cmd/fscrypt/status.go index d10dfd8..54c6f1f 100644 --- a/cmd/fscrypt/status.go +++ b/cmd/fscrypt/status.go @@ -165,9 +165,18 @@ func writeFilesystemStatus(w io.Writer, ctx *actions.Context) error { return err } - fmt.Fprintf(w, "%s filesystem %q has %s and %s\n\n", ctx.Mount.FilesystemType, + fmt.Fprintf(w, "%s filesystem %q has %s and %s.\n", ctx.Mount.FilesystemType, ctx.Mount.Path, pluralize(len(options), "protector"), pluralize(len(policyDescriptors), "policy")) + if setupMode, user, err := ctx.Mount.GetSetupMode(); err == nil { + switch setupMode { + case filesystem.WorldWritable: + fmt.Fprintf(w, "All users can create fscrypt metadata on this filesystem.\n") + case filesystem.SingleUserWritable: + fmt.Fprintf(w, "Only %s can create fscrypt metadata on this filesystem.\n", user.Username) + } + } + fmt.Fprintf(w, "\n") if len(options) > 0 { writeOptions(w, options) -- cgit v1.2.3 From 74e870b7bd1585b4b509da47e0e75db66336e576 Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: Strictly validate metadata file ownership by default The metadata validation checks introduced by the previous commits are good, but to reduce the attack surface it would be much better to avoid reading and parsing files owned by other users in the first place. There are some possible use cases for users sharing fscrypt metadata files, but I think that for the vast majority of users it is unneeded and just opens up attack surface. Thus, make fscrypt (and pam_fscrypt) not process policies or protectors owned by other users by default. Specifically, * If fscrypt or pam_fscrypt is running as a non-root user, only policies and protectors owned by the user or by root can be used. * If fscrypt is running as root, any policy or protector can be used. (This is to match user expectations -- starting a sudo session should gain rights, not remove rights.) * If pam_fscrypt is running as root, only policies and protectors owned by root can be used. Note that this only applies when the root user themselves has an fscrypt login protector, which is rare. Add an option 'allow_cross_user_metadata' to /etc/fscrypt.conf which allows restoring the old behavior for anyone who really needs it. --- cmd/fscrypt/status.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'cmd/fscrypt') diff --git a/cmd/fscrypt/status.go b/cmd/fscrypt/status.go index 54c6f1f..ed5a764 100644 --- a/cmd/fscrypt/status.go +++ b/cmd/fscrypt/status.go @@ -160,14 +160,18 @@ func writeFilesystemStatus(w io.Writer, ctx *actions.Context) error { return err } - policyDescriptors, err := ctx.Mount.ListPolicies() + policyDescriptors, err := ctx.Mount.ListPolicies(ctx.TrustedUser) if err != nil { return err } - fmt.Fprintf(w, "%s filesystem %q has %s and %s.\n", ctx.Mount.FilesystemType, + filterDescription := "" + if ctx.TrustedUser != nil { + filterDescription = fmt.Sprintf(" (only including ones owned by %s or root)", ctx.TrustedUser.Username) + } + fmt.Fprintf(w, "%s filesystem %q has %s and %s%s.\n", ctx.Mount.FilesystemType, ctx.Mount.Path, pluralize(len(options), "protector"), - pluralize(len(policyDescriptors), "policy")) + pluralize(len(policyDescriptors), "policy"), filterDescription) if setupMode, user, err := ctx.Mount.GetSetupMode(); err == nil { switch setupMode { case filesystem.WorldWritable: -- cgit v1.2.3 From 85a747493ff368a72f511619ecd391016ecb933c Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: Extend ownership validation to entire directory structure A previous commit extended file ownership validation to policy and protector files (by default -- there's an opt-out in /etc/fscrypt.conf). However, that didn't apply to the parent directories: MOUNTPOINT MOUNTPOINT/.fscrypt MOUNTPOINT/.fscrypt/policies MOUNTPOINT/.fscrypt/protectors The problem is that if the parent directories aren't trusted (owned by another non-root user), then untrusted changes to their contents can be made at any time, including the introduction of symlinks and so on. While it's debatable how much of a problem this really is, given the other validations that are done, it seems to be appropriate to validate the parent directories too. Therefore, this commit applies the same ownership validations to the above four directories as are done on the metadata files themselves. In addition, it is validated that none of these directories are symlinks except for ".fscrypt" where this is explicitly supported. --- cmd/fscrypt/setup.go | 2 +- cmd/fscrypt/status.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'cmd/fscrypt') diff --git a/cmd/fscrypt/setup.go b/cmd/fscrypt/setup.go index 1da0d16..b9a16e8 100644 --- a/cmd/fscrypt/setup.go +++ b/cmd/fscrypt/setup.go @@ -83,7 +83,7 @@ func setupFilesystem(w io.Writer, path string) error { } username := ctx.TargetUser.Username - err = ctx.Mount.CheckSetup() + err = ctx.Mount.CheckSetup(ctx.TrustedUser) if err == nil { return &filesystem.ErrAlreadySetup{Mount: ctx.Mount} } diff --git a/cmd/fscrypt/status.go b/cmd/fscrypt/status.go index ed5a764..bc8f1ee 100644 --- a/cmd/fscrypt/status.go +++ b/cmd/fscrypt/status.go @@ -101,7 +101,7 @@ func writeGlobalStatus(w io.Writer) error { t := makeTableWriter(w, "MOUNTPOINT\tDEVICE\tFILESYSTEM\tENCRYPTION\tFSCRYPT") for _, mount := range mounts { // Only print mountpoints backed by devices or using fscrypt. - usingFscrypt := mount.CheckSetup() == nil + usingFscrypt := mount.CheckSetup(nil) == nil if !usingFscrypt && mount.Device == "" { continue } -- cgit v1.2.3 From d4ce0b892cbe68db9f90f4015342e6a9069b079c Mon Sep 17 00:00:00 2001 From: Eric Biggers Date: Wed, 23 Feb 2022 12:35:04 -0800 Subject: Make all new metadata files owned by user when needed Since commit 4c7c6631cc5a ("Set owner of login protectors to correct user"), login protectors are made owned by the user when root creates one on a user's behalf. That's good, but the same isn't true of other files that get created at the same time: - The policy protecting the directory - The protector link file, if the policy is on a different filesystem - The recovery protector, if the policy is on a different filesystem - The recovery instructions file In preparation for setting all metadata files to mode 0600, start making all these files owned by the user in this scenario as well. --- cmd/fscrypt/protector.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'cmd/fscrypt') diff --git a/cmd/fscrypt/protector.go b/cmd/fscrypt/protector.go index ac864dd..186ca7a 100644 --- a/cmd/fscrypt/protector.go +++ b/cmd/fscrypt/protector.go @@ -23,6 +23,7 @@ package main import ( "fmt" "log" + "os/user" "github.com/google/fscrypt/actions" "github.com/google/fscrypt/filesystem" @@ -38,7 +39,6 @@ func createProtectorFromContext(ctx *actions.Context) (*actions.Protector, error return nil, err } log.Printf("using source: %s", ctx.Config.Source.String()) - if ctx.Config.Source == metadata.SourceType_pam_passphrase { if userFlag.Value == "" && util.IsUserRoot() { return nil, ErrSpecifyUser @@ -70,7 +70,11 @@ IMPORTANT: Before continuing, ensure you have properly set up your system for } } - return actions.CreateProtector(ctx, name, createKeyFn) + var owner *user.User + if ctx.Config.Source == metadata.SourceType_pam_passphrase && util.IsUserRoot() { + owner = ctx.TargetUser + } + return actions.CreateProtector(ctx, name, createKeyFn, owner) } // selectExistingProtector returns a locked Protector which corresponds to an -- cgit v1.2.3