Soft chroot jail for sftp-server

Dimitri Nüscheler dimitri.nuescheler at sunrise.ch
Wed Jan 1 13:12:58 EST 2014


Hi everyone

I would like to enable unprivileged users to share only certain
directories using SFTP without acquiring root, without setting
capabilities using public-key-based forced commands.

In another use case unprivileged users could write scripts that
evaluate "$SSH_ORIGINAL_COMMAND" and then either execute sftp-server
in a jail "$SSH_ORIGINAL_COMMAND" after "review" (e.g. matches a
certain regexp) and fortification.

External users would access the system in a user-defined restricted way to:
upload data, trigger processing of data, download processed data.

I created a patch against the debian sid distribution of OpenSSH
6.4-p1-1 "sftp-server.c" that introduces a command line option "-j"
(original file and patch attached)

It's not yet in a fully polished, robust and tested state, but I think
at least it is serves as PoC.

Would you accept such a patch if finalized?
Do you have any recommendations
- regarding my patch?
- to what I would like to achieve?
(basic C programming hints welcome too, I usually don't code in C,
first serious use)

Thank you & Kind Regards

Dimitri
-------------- next part --------------
75a76,78
> /* Restrict access to this directory */
> char* jail;
> 
85a89,213
> /* Concatenate 2 path parts in a way that one doesn't need to care about leading/trailing slashes. Returned pointer can be freed. */
> static char* concat_path(char* parent, char* child) {
> 	size_t parent_len = strlen(parent);
> 	if (parent_len < 1)
> 		return xstrdup(child);
> 	size_t child_len = strlen(child);
> 	if (child_len < 1)
> 		return xstrdup(parent);
> 	
> 	if (*child == '/') {
> 		child++;
> 		child_len--;
> 	}
> 	
> 	char* cat;
> 	if (*(parent + parent_len - 1) == '/') {
> 		size_t cat_len = sizeof(char) * (parent_len + child_len + 1);
> 		cat = xmalloc(sizeof(char) * cat_len);
> 		*cat = '\0';
> 		strlcat(cat,parent,cat_len);
> 		strlcat(cat,child,cat_len);
> 		return cat;
> 	} else {
> 		size_t cat_len = sizeof(char) * (parent_len + child_len + 2);
> 		cat = xmalloc(sizeof(char) * cat_len);
> 		*cat = '\0';
> 		strlcat(cat,parent,cat_len);
> 		strlcat(cat,"/",cat_len);
> 		strlcat(cat,child,cat_len);
> 		return cat;
> 	}
> }
> 
> /* shorten the path by removing occurences of "//" and any relative (backward) directory references (".", ".."), ignoring backward references
>  * that traverse the current directory or root, maintaining leading "/" and maintaining trailing "/" if possible.
>  * The passed string is modified.
>  */
> static char* shorten_path(char* path) {
> 	unsigned int path_len = strlen(path)+1;
> 	//char* short_path = path; malloc(sizeof(char) * short_path_len);
> 	size_t j = 0;
> 	size_t i = 0;
> 	//Make sure path doesn't contain any "//"
> 	for (i = 0; i < path_len; i++) {
> 		if (*(path+i) == '/' && j>0 && *(path+j-1) == '/')
> 			j--;
> 		
> 		*(path+j) = *(path+i);
> 		j++;
> 	}
> 	
> 	j = 0;
> 	int dot_count = 0;
> 	i = 0;
> 	path_len = strlen(path)+1;
> 	char* real_path = path;
> 	if (*path == '/') {
> 		path++;
> 		path_len--;
> 	}
> 	
> 	// Execute actual ".." resolution with a path that doesn't care if it is absolute or relative
> 	for (i = 0; i < path_len; i++) {
> 		if (*(path+i) == '/' || *(path+i) == '\0') {
> 			if (dot_count == 2) {
> 				while (j>0 && *(path+j-1) != '/') {
> 					j--;	
> 				}
> 				if (j>0) j--;
> 				while (j>0 && *(path+j-1) != '/') {
> 					j--;	
> 				}
> 				if (j<1 && *(path+i) == '/') i++;
> 			}
> 			else if (dot_count == 1) {
> 				while (j>0 && *(path+j-1) != '/') {
> 					j--;	
> 				}
> 				if (j<1 && *(path+i) == '/') i++;
> 			}
> 			dot_count = *(path+i) == '.' ? 1 : 0;
> 		}
> 		else if (*(path+i) == '.') dot_count++;
> 		else dot_count = 3;
> 		
> 		if (*(path+i) == '/' && j>0 && *(path+j-1) == '/')
> 		j--;
> 		
> 		*(path+j) = *(path+i);
> 		j++;
> 	}
> 	return real_path;
> }
> /* Takes a path from jail perspective and converts it to the actual path. This function frees path (so expects a freeable pointer) or reuses it so in any case returns a freeable pointer. */
> static char* jail_to_actual(char* path) {
> 	if (jail == NULL) return path;
> 	char* shortened = shorten_path(path);
> 	char* translated = concat_path(jail,shortened);
> 	free(shortened);
> 	return translated;
> }
> /* The opposite of jail_to_actual, returns NULL if path is not in jail, This function frees path (so expects a freeable pointer) or reuses it so in any case returns a freeable pointer. */
> static char* actual_to_jail(char* path) {
> 	if (jail == NULL) return path;
> 	unsigned int jail_len = strlen(jail);
> 	if (strncmp(jail,path,jail_len) != 0) return NULL;
> 	char* actual = xstrdup(path+jail_len);
> 	free(path);
> 	if (strlen(actual) < 1) {
> 		free(actual);
> 		actual = xstrdup("/");
> 	}
> 	return actual;
> }
> /* Removes trailing slashes, except a leading one, modifies the passed string */
> static void rtrim_slash(char* path) {
> 	unsigned int len = strlen(path);
> 	int i;
> 	for (i = len-1; i > 0; i--) {
> 		if (*(path+i) == '/')
> 			*(path+i) = '\0';
> 		else break;
> 	}
> }
> 
523d650
< 
552d678
< 
554a681,696
> 	name = jail_to_actual(name);
> 	if (jail != NULL) {
> 		char resolvedname[MAXPATHLEN];
> 		if (realpath(name, resolvedname) == NULL) {
> 			send_status(id, errno_to_portable(errno));
> 			free(name);
> 			return;
> 		}
> 		char* jailed_resolvedname = actual_to_jail(xstrdup(resolvedname));
> 		if (jailed_resolvedname == NULL) {
> 			send_status(id,SSH2_FX_FAILURE);
> 			free(name);
> 			return;
> 		}
> 		
> 	}
589d730
< 
695a837
> 	name = jail_to_actual(name);
771a914
> 	name = jail_to_actual(name);
889a1033,1048
> 	path = jail_to_actual(path);
> 	if (jail != NULL) {
> 		char resolvedname[MAXPATHLEN];
> 		if (realpath(path, resolvedname) == NULL) {
> 			send_status(id, errno_to_portable(errno));
> 			free(path);
> 			return;
> 		}
> 		char* jailed_resolvedname = actual_to_jail(xstrdup(resolvedname));
> 		if (jailed_resolvedname == NULL) {
> 			send_status(id,SSH2_FX_FAILURE);
> 			free(path);
> 			return;
> 		}
> 		
> 	}
975a1135
> 	name = jail_to_actual(name);
997a1158
> 	name = jail_to_actual(name);
1021a1183
> 	name = jail_to_actual(name);
1042a1205
> 	path = jail_to_actual(path);
1054,1055c1217,1233
< 		s.name = s.long_name = resolvedname;
< 		send_names(id, 1, &s);
---
> 		if (jail != NULL) {
> 			char* jailed_resolvedname = actual_to_jail(xstrdup(resolvedname));
> 			/* Note that only the resolved string needs to point inside the jail. During resolution it may visit links outside jail
> 			 * The SFTP-user, however, is not able to create links to the outside anyway.
> 			 */
> 			if (jailed_resolvedname == NULL) send_status(id,SSH2_FX_FAILURE);
> 			else {
> 				s.name = s.long_name = jailed_resolvedname;
> 				send_names(id, 1, &s);
> 			}
> 			free(jailed_resolvedname);
> 		}
> 		else {
> 			s.name = s.long_name = resolvedname;
> 			send_names(id, 1, &s);
> 		}
> 		
1070a1249,1250
> 	oldpath = jail_to_actual(oldpath);
> 	newpath = jail_to_actual(newpath);
1131a1312
> 	path = jail_to_actual(path);
1156a1338,1339
> 	oldpath = jail_to_actual(oldpath);
> 	newpath = jail_to_actual(newpath);
1178a1362,1363
> 	oldpath = jail_to_actual(oldpath);
> 	newpath = jail_to_actual(newpath);
1198a1384
> 	path = jail_to_actual(path);
1235a1422,1423
> 	oldpath = jail_to_actual(oldpath);
> 	newpath = jail_to_actual(newpath);
1406a1595
> 	jail = NULL;
1414d1602
< 
1417c1605
< 	while (!skipargs && (ch = getopt(argc, argv, "d:f:l:u:cehR")) != -1) {
---
> 	while (!skipargs && (ch = getopt(argc, argv, "d:f:l:u:j:cehR")) != -1) {
1455a1644,1657
> 		case 'j':
> 			if (strlen(optarg) > 0 && *optarg == '/')
> 				jail = xstrdup(optarg);
> 			else {
> 				char* wd = get_current_dir_name();
> 				jail = concat_path(wd,optarg);
> 				free(wd);
> 			}
> 			rtrim_slash(jail);
> 			if (*jail == '\0' || (strlen(jail) == 1 && *jail == '/' )) {
> 				jail = NULL;
> 				break;
> 			}
> 			break;
1498c1700
< 
---
> 	
1500,1501c1702,1709
< 		if (chdir(homedir) != 0) {
< 			error("chdir to \"%s\" failed: %s", homedir,
---
> 		char* jailed_homedir;
> 		if (jail != NULL) {
> 			jailed_homedir = concat_path(jail,homedir);
> 		}
> 		else
> 			jailed_homedir = xstrdup(homedir);
> 		if (chdir(jailed_homedir) != 0) {
> 			error("chdir to \"%s\" failed: %s", jailed_homedir,
1503a1712
> 		free(jailed_homedir);
-------------- next part --------------
A non-text attachment was scrubbed...
Name: sftp-server.c
Type: text/x-csrc
Size: 33990 bytes
Desc: not available
URL: <http://lists.mindrot.org/pipermail/openssh-unix-dev/attachments/20140101/8703378a/attachment-0001.bin>


More information about the openssh-unix-dev mailing list