From b6c8da512960cfc7e202d02bb94ced87a9b92bd0 Mon Sep 17 00:00:00 2001
From: Colin Watson <cjwatson@debian.org>
Date: Sun, 9 Feb 2014 16:09:58 +0000
Subject: Allow harmless group-writability

Allow secure files (~/.ssh/config, ~/.ssh/authorized_keys, etc.) to be
group-writable, provided that the group in question contains only the file's
owner.  Rejected upstream for IMO incorrect reasons (e.g. a misunderstanding
about the contents of gr->gr_mem).  Given that per-user groups and umask 002
are the default setup in Debian (for good reasons - this makes operating in
setgid directories with other groups much easier), we need to permit this by
default.

Bug: https://bugzilla.mindrot.org/show_bug.cgi?id=1060
Bug-Debian: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=314347
Last-Update: 2025-10-06

Patch-Name: user-group-modes.patch
---
 auth-rhosts.c |  6 ++----
 auth.c        |  3 +--
 misc.c        | 55 +++++++++++++++++++++++++++++++++++++++++++++++----
 misc.h        |  2 ++
 readconf.c    |  3 +--
 ssh.1         |  2 ++
 ssh_config.5  |  2 ++
 7 files changed, 61 insertions(+), 12 deletions(-)

diff --git a/auth-rhosts.c b/auth-rhosts.c
index 031186f24..560a0b894 100644
--- a/auth-rhosts.c
+++ b/auth-rhosts.c
@@ -263,8 +263,7 @@ auth_rhosts2(struct passwd *pw, const char *client_user, const char *hostname,
 		return 0;
 	}
 	if (options.strict_modes &&
-	    ((st.st_uid != 0 && st.st_uid != pw->pw_uid) ||
-	    (st.st_mode & 022) != 0)) {
+	    !secure_permissions(&st, pw->pw_uid)) {
 		logit("Rhosts authentication refused for %.100s: "
 		    "bad ownership or modes for home directory.", pw->pw_name);
 		auth_debug_add("Rhosts authentication refused for %.100s: "
@@ -293,8 +292,7 @@ auth_rhosts2(struct passwd *pw, const char *client_user, const char *hostname,
 		 * allowing access to their account by anyone.
 		 */
 		if (options.strict_modes &&
-		    ((st.st_uid != 0 && st.st_uid != pw->pw_uid) ||
-		    (st.st_mode & 022) != 0)) {
+		    !secure_permissions(&st, pw->pw_uid)) {
 			logit("Rhosts authentication refused for %.100s: "
 			    "bad modes for %.200s", pw->pw_name, path);
 			auth_debug_add("Bad file modes for %.200s", path);
diff --git a/auth.c b/auth.c
index d25653ee4..f075355e7 100644
--- a/auth.c
+++ b/auth.c
@@ -428,8 +428,7 @@ check_key_in_hostfiles(struct passwd *pw, struct sshkey *key, const char *host,
 		user_hostfile = tilde_expand_filename(userfile, pw->pw_uid);
 		if (options.strict_modes &&
 		    (stat(user_hostfile, &st) == 0) &&
-		    ((st.st_uid != 0 && st.st_uid != pw->pw_uid) ||
-		    (st.st_mode & 022) != 0)) {
+		    !secure_permissions(&st, pw->pw_uid)) {
 			logit("Authentication refused for %.100s: "
 			    "bad owner or modes for %.200s",
 			    pw->pw_name, user_hostfile);
diff --git a/misc.c b/misc.c
index ce77ec943..3c1a6751f 100644
--- a/misc.c
+++ b/misc.c
@@ -1466,6 +1466,55 @@ percent_dollar_expand(const char *string, ...)
 	return ret;
 }
 
+int
+secure_permissions(struct stat *st, uid_t uid)
+{
+	if (!platform_sys_dir_uid(st->st_uid) && st->st_uid != uid)
+		return 0;
+	if ((st->st_mode & 002) != 0)
+		return 0;
+	if ((st->st_mode & 020) != 0) {
+		/* If the file is group-writable, the group in question must
+		 * have exactly one member, namely the file's owner.
+		 * (Zero-member groups are typically used by setgid
+		 * binaries, and are unlikely to be suitable.)
+		 */
+		struct passwd *pw;
+		struct group *gr;
+		int members = 0;
+
+		gr = getgrgid(st->st_gid);
+		if (!gr)
+			return 0;
+
+		/* Check primary group memberships. */
+		while ((pw = getpwent()) != NULL) {
+			if (pw->pw_gid == gr->gr_gid) {
+				++members;
+				if (pw->pw_uid != uid)
+					return 0;
+			}
+		}
+		endpwent();
+
+		pw = getpwuid(st->st_uid);
+		if (!pw)
+			return 0;
+
+		/* Check supplementary group memberships. */
+		if (gr->gr_mem[0]) {
+			++members;
+			if (strcmp(pw->pw_name, gr->gr_mem[0]) ||
+			    gr->gr_mem[1])
+				return 0;
+		}
+
+		if (!members)
+			return 0;
+	}
+	return 1;
+}
+
 int
 tun_open(int tun, int mode, char **ifname)
 {
@@ -2288,8 +2337,7 @@ safe_path(const char *name, struct stat *stp, const char *pw_dir,
 		snprintf(err, errlen, "%s is not a regular file", buf);
 		return -1;
 	}
-	if ((!platform_sys_dir_uid(stp->st_uid) && stp->st_uid != uid) ||
-	    (stp->st_mode & 022) != 0) {
+	if (!secure_permissions(stp, uid)) {
 		snprintf(err, errlen, "bad ownership or modes for file %s",
 		    buf);
 		return -1;
@@ -2309,8 +2357,7 @@ safe_path(const char *name, struct stat *stp, const char *pw_dir,
 		strlcpy(buf, cp, sizeof(buf));
 
 		if (stat(buf, &st) == -1 ||
-		    (!platform_sys_dir_uid(st.st_uid) && st.st_uid != uid) ||
-		    (st.st_mode & 022) != 0) {
+		    !secure_permissions(&st, uid)) {
 			snprintf(err, errlen,
 			    "bad ownership or modes for directory %s", buf);
 			return -1;
diff --git a/misc.h b/misc.h
index f3c5a18c6..aad15dfbd 100644
--- a/misc.h
+++ b/misc.h
@@ -254,6 +254,8 @@ struct notifier_ctx *notify_start(int, const char *, ...)
 void	notify_complete(struct notifier_ctx *, const char *, ...)
 	__attribute__((format(printf, 2, 3)));
 
+int	 secure_permissions(struct stat *st, uid_t uid);
+
 #define MINIMUM(a, b)	(((a) < (b)) ? (a) : (b))
 #define MAXIMUM(a, b)	(((a) > (b)) ? (a) : (b))
 #define ROUNDUP(x, y)   ((((x)+((y)-1))/(y))*(y))
diff --git a/readconf.c b/readconf.c
index 95d497938..c568697cf 100644
--- a/readconf.c
+++ b/readconf.c
@@ -2674,8 +2674,7 @@ read_config_file_depth(const char *filename, struct passwd *pw,
 
 		if (fstat(fileno(f), &sb) == -1)
 			fatal("fstat %s: %s", filename, strerror(errno));
-		if (((sb.st_uid != 0 && sb.st_uid != getuid()) ||
-		    (sb.st_mode & 022) != 0))
+		if (!secure_permissions(&sb, getuid()))
 			fatal("Bad owner or permissions on %s", filename);
 	}
 
diff --git a/ssh.1 b/ssh.1
index f83514c8f..62bb40a50 100644
--- a/ssh.1
+++ b/ssh.1
@@ -1582,6 +1582,8 @@ The file format and configuration options are described in
 .Xr ssh_config 5 .
 Because of the potential for abuse, this file must have strict permissions:
 read/write for the user, and not writable by others.
+It may be group-writable provided that the group in question contains only
+the user.
 .Pp
 .It Pa ~/.ssh/environment
 Contains additional definitions for environment variables; see
diff --git a/ssh_config.5 b/ssh_config.5
index 718c870f8..c947aba0a 100644
--- a/ssh_config.5
+++ b/ssh_config.5
@@ -2512,6 +2512,8 @@ The format of this file is described above.
 This file is used by the SSH client.
 Because of the potential for abuse, this file must have strict permissions:
 read/write for the user, and not writable by others.
+It may be group-writable provided that the group in question contains only
+the user.
 .It Pa /etc/ssh/ssh_config
 Systemwide configuration file.
 This file provides defaults for those
