Skip to content

Commit d79374c

Browse files
author
Junio C Hamano
committed
[PATCH] daemon.c and path.enter_repo(): revamp path validation.
The whitelist of git-daemon is checked against return value from enter_repo(), and enter_repo() used to return the value obtained from getcwd() to avoid directory aliasing issues as discussed earier (mid October 2005). Unfortunately, it did not go well as we hoped. For example, /pub on a kernel.org public machine is a symlink to its real mountpoint, and it is understandable that the administrator does not want to adjust the whitelist every time /pub needs to point at a different partition for storage allcation or whatever reasons. Being able to keep using /pub/scm as the whitelist is a desirable property. So this version of enter_repo() reports what it used to chdir() and validate, but does not use getcwd() to canonicalize the directory name. When it sees a user relative path ~user/path, it internally resolves it to try chdir() there, but it still reports ~user/path (possibly after appending .git if allowed to do so, in which case it would report ~user/path.git). What this means is that if a whitelist wants to allow a user relative path, it needs to say "~" (for all users) or list user home directories like "~alice" "~bob". And no, you cannot say /home if the advertised way to access user home directories are ~alice,~bob, etc. The whole point of this is to avoid unnecessary aliasing issues. Anyway, because of this, daemon needs to do a bit more work to guard itself. Namely, it needs to make sure that the accessor does not try to exploit its leading path match rule by inserting /../ in the middle or hanging /.. at the end. I resurrected the belts and suspender paranoia code HPA did for this purpose. This check cannot be done in the enter_repo() unconditionally, because there are valid callers of enter_repo() that want to honor /../; authorized users coming over ssh to run send-pack and fetch-pack should be allowed to do so. Signed-off-by: Junio C Hamano <junkio@cox.net>
1 parent 7950571 commit d79374c

File tree

2 files changed

+159
-58
lines changed

2 files changed

+159
-58
lines changed

daemon.c

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,63 @@ static void loginfo(const char *err, ...)
8282
va_end(params);
8383
}
8484

85+
static int avoid_alias(char *p)
86+
{
87+
int sl, ndot;
88+
89+
/*
90+
* This resurrects the belts and suspenders paranoia check by HPA
91+
* done in <435560F7.4080006@zytor.com> thread, now enter_repo()
92+
* does not do getcwd() based path canonicalizations.
93+
*
94+
* sl becomes true immediately after seeing '/' and continues to
95+
* be true as long as dots continue after that without intervening
96+
* non-dot character.
97+
*/
98+
if (!p || (*p != '/' && *p != '~'))
99+
return -1;
100+
sl = 1; ndot = 0;
101+
p++;
102+
103+
while (1) {
104+
char ch = *p++;
105+
if (sl) {
106+
if (ch == '.')
107+
ndot++;
108+
else if (ch == '/') {
109+
if (ndot < 3)
110+
/* reject //, /./ and /../ */
111+
return -1;
112+
ndot = 0;
113+
}
114+
else if (ch == 0) {
115+
if (0 < ndot && ndot < 3)
116+
/* reject /.$ and /..$ */
117+
return -1;
118+
return 0;
119+
}
120+
else
121+
sl = ndot = 0;
122+
}
123+
else if (ch == 0)
124+
return 0;
125+
else if (ch == '/') {
126+
sl = 1;
127+
ndot = 0;
128+
}
129+
}
130+
}
131+
85132
static char *path_ok(char *dir)
86133
{
87-
char *path = enter_repo(dir, strict_paths);
134+
char *path;
135+
136+
if (avoid_alias(dir)) {
137+
logerror("'%s': aliased", dir);
138+
return NULL;
139+
}
140+
141+
path = enter_repo(dir, strict_paths);
88142

89143
if (!path) {
90144
logerror("'%s': unable to chdir or not a git archive", dir);
@@ -96,9 +150,11 @@ static char *path_ok(char *dir)
96150
int pathlen = strlen(path);
97151

98152
/* The validation is done on the paths after enter_repo
99-
* canonicalization, so whitelist should be written in
100-
* terms of real pathnames (i.e. after ~user is expanded
101-
* and symlinks resolved).
153+
* appends optional {.git,.git/.git} and friends, but
154+
* it does not use getcwd(). So if your /pub is
155+
* a symlink to /mnt/pub, you can whitelist /pub and
156+
* do not have to say /mnt/pub.
157+
* Do not say /pub/.
102158
*/
103159
for ( pp = ok_paths ; *pp ; pp++ ) {
104160
int len = strlen(*pp);

path.c

Lines changed: 99 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -131,76 +131,121 @@ int validate_symref(const char *path)
131131
return -1;
132132
}
133133

134-
static char *current_dir(void)
134+
static char *user_path(char *buf, char *path, int sz)
135135
{
136-
return getcwd(pathname, sizeof(pathname));
137-
}
138-
139-
static int user_chdir(char *path)
140-
{
141-
char *dir = path;
136+
struct passwd *pw;
137+
char *slash;
138+
int len, baselen;
142139

143-
if(*dir == '~') { /* user-relative path */
144-
struct passwd *pw;
145-
char *slash = strchr(dir, '/');
146-
147-
dir++;
148-
/* '~/' and '~' (no slash) means users own home-dir */
149-
if(!*dir || *dir == '/')
150-
pw = getpwuid(getuid());
151-
else {
152-
if (slash) {
153-
*slash = '\0';
154-
pw = getpwnam(dir);
155-
*slash = '/';
156-
}
157-
else
158-
pw = getpwnam(dir);
140+
if (!path || path[0] != '~')
141+
return NULL;
142+
path++;
143+
slash = strchr(path, '/');
144+
if (path[0] == '/' || !path[0]) {
145+
pw = getpwuid(getuid());
146+
}
147+
else {
148+
if (slash) {
149+
*slash = 0;
150+
pw = getpwnam(path);
151+
*slash = '/';
159152
}
160-
161-
/* make sure we got something back that we can chdir() to */
162-
if(!pw || chdir(pw->pw_dir) < 0)
163-
return -1;
164-
165-
if(!slash || !slash[1]) /* no path following username */
166-
return 0;
167-
168-
dir = slash + 1;
153+
else
154+
pw = getpwnam(path);
169155
}
170-
171-
/* ~foo/path/to/repo is now path/to/repo and we're in foo's homedir */
172-
if(chdir(dir) < 0)
173-
return -1;
174-
175-
return 0;
156+
if (!pw || !pw->pw_dir || sz <= strlen(pw->pw_dir))
157+
return NULL;
158+
baselen = strlen(pw->pw_dir);
159+
memcpy(buf, pw->pw_dir, baselen);
160+
while ((1 < baselen) && (buf[baselen-1] == '/')) {
161+
buf[baselen-1] = 0;
162+
baselen--;
163+
}
164+
if (slash && slash[1]) {
165+
len = strlen(slash);
166+
if (sz <= baselen + len)
167+
return NULL;
168+
memcpy(buf + baselen, slash, len + 1);
169+
}
170+
return buf;
176171
}
177172

173+
/*
174+
* First, one directory to try is determined by the following algorithm.
175+
*
176+
* (0) If "strict" is given, the path is used as given and no DWIM is
177+
* done. Otherwise:
178+
* (1) "~/path" to mean path under the running user's home directory;
179+
* (2) "~user/path" to mean path under named user's home directory;
180+
* (3) "relative/path" to mean cwd relative directory; or
181+
* (4) "/absolute/path" to mean absolute directory.
182+
*
183+
* Unless "strict" is given, we try access() for existence of "%s.git/.git",
184+
* "%s/.git", "%s.git", "%s" in this order. The first one that exists is
185+
* what we try.
186+
*
187+
* Second, we try chdir() to that. Upon failure, we return NULL.
188+
*
189+
* Then, we try if the current directory is a valid git repository.
190+
* Upon failure, we return NULL.
191+
*
192+
* If all goes well, we return the directory we used to chdir() (but
193+
* before ~user is expanded), avoiding getcwd() resolving symbolic
194+
* links. User relative paths are also returned as they are given,
195+
* except DWIM suffixing.
196+
*/
178197
char *enter_repo(char *path, int strict)
179198
{
180-
if(!path)
199+
static char used_path[PATH_MAX];
200+
static char validated_path[PATH_MAX];
201+
202+
if (!path)
181203
return NULL;
182204

183-
if (strict) {
184-
if (chdir(path) < 0)
205+
if (!strict) {
206+
static const char *suffix[] = {
207+
".git/.git", "/.git", ".git", "", NULL,
208+
};
209+
int len = strlen(path);
210+
int i;
211+
while ((1 < len) && (path[len-1] == '/')) {
212+
path[len-1] = 0;
213+
len--;
214+
}
215+
if (PATH_MAX <= len)
185216
return NULL;
186-
}
187-
else {
188-
if (!*path)
189-
; /* happy -- no chdir */
190-
else if (!user_chdir(path))
191-
; /* happy -- as given */
192-
else if (!user_chdir(mkpath("%s.git", path)))
193-
; /* happy -- uemacs --> uemacs.git */
194-
else
217+
if (path[0] == '~') {
218+
if (!user_path(used_path, path, PATH_MAX))
219+
return NULL;
220+
strcpy(validated_path, path);
221+
path = used_path;
222+
}
223+
else if (PATH_MAX - 10 < len)
224+
return NULL;
225+
else {
226+
path = strcpy(used_path, path);
227+
strcpy(validated_path, path);
228+
}
229+
len = strlen(path);
230+
for (i = 0; suffix[i]; i++) {
231+
strcpy(path + len, suffix[i]);
232+
if (!access(path, F_OK)) {
233+
strcat(validated_path, suffix[i]);
234+
break;
235+
}
236+
}
237+
if (!suffix[i] || chdir(path))
195238
return NULL;
196-
(void)chdir(".git");
239+
path = validated_path;
197240
}
241+
else if (chdir(path))
242+
return NULL;
198243

199-
if(access("objects", X_OK) == 0 && access("refs", X_OK) == 0 &&
200-
validate_symref("HEAD") == 0) {
244+
if (access("objects", X_OK) == 0 && access("refs", X_OK) == 0 &&
245+
validate_symref("HEAD") == 0) {
201246
putenv("GIT_DIR=.");
202247
check_repository_format();
203-
return current_dir();
248+
return path;
204249
}
205250

206251
return NULL;

0 commit comments

Comments
 (0)