Skip to content

Commit 65f4305

Browse files
author
Edward Thomson
committed
checkout: disallow bad paths on HFS
HFS filesystems ignore some characters like U+200C. When these characters are included in a path, they will be ignored for the purposes of comparison with other paths. Thus, if you have a ".git" folder, a folder of ".git<U+200C>" will also match. Protect our ".git" folder by ensuring that ".git<U+200C>" and friends do not match it.
1 parent 492bcd0 commit 65f4305

File tree

53 files changed

+207
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+207
-1
lines changed

src/path.c

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,95 @@ GIT_INLINE(bool) verify_dospath(
12821282
component[last] != ':');
12831283
}
12841284

1285+
GIT_INLINE(bool) verify_dotgit_hfs(const char *component, size_t len)
1286+
{
1287+
const unsigned char *c;
1288+
int git = 0, ign = 0;
1289+
unsigned char one, two;
1290+
1291+
while (len) {
1292+
switch (*(c = (const unsigned char *)component++)) {
1293+
case '.':
1294+
if (ign || git++ != 0)
1295+
return true;
1296+
break;
1297+
case 'g':
1298+
case 'G':
1299+
if (ign || git++ != 1)
1300+
return true;
1301+
break;
1302+
case 'i':
1303+
case 'I':
1304+
if (ign || git++ != 2)
1305+
return true;
1306+
break;
1307+
case 't':
1308+
case 'T':
1309+
if (ign || git++ != 3)
1310+
return true;
1311+
break;
1312+
1313+
case 0xe2:
1314+
case 0xef:
1315+
if (ign++ != 0)
1316+
return true;
1317+
one = *c;
1318+
break;
1319+
1320+
case 0x80:
1321+
case 0x81:
1322+
if (ign++ != 1 || one != 0xe2)
1323+
return true;
1324+
two = *c;
1325+
break;
1326+
1327+
case 0xbb:
1328+
if (ign++ != 1 || one != 0xef)
1329+
return true;
1330+
two = *c;
1331+
break;
1332+
1333+
case 0x8c:
1334+
case 0x8d:
1335+
case 0x8e:
1336+
case 0x8f:
1337+
if (ign != 2 || two != 0x80)
1338+
return true;
1339+
ign = 0;
1340+
break;
1341+
1342+
case 0xaa:
1343+
case 0xab:
1344+
case 0xac:
1345+
case 0xad:
1346+
case 0xae:
1347+
if (ign != 2 || (two != 0x80 && two != 0x81))
1348+
return true;
1349+
ign = 0;
1350+
break;
1351+
1352+
case 0xaf:
1353+
if (ign != 2 || two != 0x81)
1354+
return true;
1355+
ign = 0;
1356+
break;
1357+
1358+
case 0xbf:
1359+
if (ign != 2 || two != 0xbb)
1360+
return true;
1361+
ign = 0;
1362+
break;
1363+
1364+
default:
1365+
return true;
1366+
}
1367+
1368+
len--;
1369+
}
1370+
1371+
return (ign || git != 4);
1372+
}
1373+
12851374
GIT_INLINE(bool) verify_char(unsigned char c, unsigned int flags)
12861375
{
12871376
if ((flags & GIT_PATH_REJECT_BACKSLASH) && c == '\\')
@@ -1362,6 +1451,10 @@ static bool verify_component(
13621451
return false;
13631452
}
13641453

1454+
if (flags & GIT_PATH_REJECT_DOT_GIT_HFS &&
1455+
!verify_dotgit_hfs(component, len))
1456+
return false;
1457+
13651458
return true;
13661459
}
13671460

src/path.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ extern int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or
472472
#define GIT_PATH_REJECT_DOS_GIT_SHORTNAME (1 << 6)
473473
#define GIT_PATH_REJECT_DOS_PATHS (1 << 7)
474474
#define GIT_PATH_REJECT_NT_CHARS (1 << 8)
475+
#define GIT_PATH_REJECT_DOT_GIT_HFS (1 << 9)
475476

476477
#ifdef GIT_WIN32
477478
# define GIT_PATH_REJECT_DEFAULTS \
@@ -483,6 +484,10 @@ extern int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or
483484
GIT_PATH_REJECT_DOS_GIT_SHORTNAME | \
484485
GIT_PATH_REJECT_DOS_PATHS | \
485486
GIT_PATH_REJECT_NT_CHARS
487+
#elif __APPLE__
488+
# define GIT_PATH_REJECT_DEFAULTS \
489+
GIT_PATH_REJECT_TRAVERSAL | \
490+
GIT_PATH_REJECT_DOT_GIT_HFS
486491
#else
487492
# define GIT_PATH_REJECT_DEFAULTS GIT_PATH_REJECT_TRAVERSAL
488493
#endif

tests/checkout/nasty.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,45 @@ void test_checkout_nasty__dot_git_colon_stuff(void)
249249
#endif
250250
}
251251

252+
/* Trees that contains entries with a tree ".git" that contain
253+
* byte sequences:
254+
* { 0xe2, 0x80, 0x8c }
255+
* { 0xe2, 0x80, 0x8d }
256+
* { 0xe2, 0x80, 0x8e }
257+
* { 0xe2, 0x80, 0x8f }
258+
* { 0xe2, 0x80, 0xaa }
259+
* { 0xe2, 0x80, 0xab }
260+
* { 0xe2, 0x80, 0xac }
261+
* { 0xe2, 0x80, 0xad }
262+
* { 0xe2, 0x81, 0xae }
263+
* { 0xe2, 0x81, 0xaa }
264+
* { 0xe2, 0x81, 0xab }
265+
* { 0xe2, 0x81, 0xac }
266+
* { 0xe2, 0x81, 0xad }
267+
* { 0xe2, 0x81, 0xae }
268+
* { 0xe2, 0x81, 0xaf }
269+
* { 0xef, 0xbb, 0xbf }
270+
* Because these map to characters that HFS filesystems "ignore". Thus
271+
* ".git<U+200C>" will map to ".git".
272+
*/
273+
void test_checkout_nasty__dot_git_hfs_ignorable(void)
274+
{
275+
#ifdef __APPLE__
276+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_1", ".git/foobar");
277+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_2", ".git/foobar");
278+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_3", ".git/foobar");
279+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_4", ".git/foobar");
280+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_5", ".git/foobar");
281+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_6", ".git/foobar");
282+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_7", ".git/foobar");
283+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_8", ".git/foobar");
284+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_9", ".git/foobar");
285+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_10", ".git/foobar");
286+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_11", ".git/foobar");
287+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_12", ".git/foobar");
288+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_13", ".git/foobar");
289+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_14", ".git/foobar");
290+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_15", ".git/foobar");
291+
test_checkout_fails("refs/heads/dotgit_hfs_ignorable_16", ".git/foobar");
292+
#endif
293+
}

tests/path/core.c

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ void test_path_core__isvalid_dos_paths_withnum(void)
223223
cl_assert_equal_b(true, git_path_isvalid(NULL, "com1\\foo", GIT_PATH_REJECT_DOS_PATHS));
224224
}
225225

226-
void test_core_path__isvalid_nt_chars(void)
226+
void test_path_core__isvalid_nt_chars(void)
227227
{
228228
cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf\001foo", 0));
229229
cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf\037bar", 0));
@@ -245,3 +245,32 @@ void test_core_path__isvalid_nt_chars(void)
245245
cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf?bar", GIT_PATH_REJECT_NT_CHARS));
246246
cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf*bar", GIT_PATH_REJECT_NT_CHARS));
247247
}
248+
249+
void test_path_core__isvalid_dotgit_with_hfs_ignorables(void)
250+
{
251+
cl_assert_equal_b(false, git_path_isvalid(NULL, ".git", GIT_PATH_REJECT_DOT_GIT_HFS));
252+
cl_assert_equal_b(false, git_path_isvalid(NULL, ".git\xe2\x80\x8c", GIT_PATH_REJECT_DOT_GIT_HFS));
253+
cl_assert_equal_b(false, git_path_isvalid(NULL, ".gi\xe2\x80\x8dT", GIT_PATH_REJECT_DOT_GIT_HFS));
254+
cl_assert_equal_b(false, git_path_isvalid(NULL, ".g\xe2\x80\x8eIt", GIT_PATH_REJECT_DOT_GIT_HFS));
255+
cl_assert_equal_b(false, git_path_isvalid(NULL, ".\xe2\x80\x8fgIt", GIT_PATH_REJECT_DOT_GIT_HFS));
256+
cl_assert_equal_b(false, git_path_isvalid(NULL, "\xe2\x80\xaa.gIt", GIT_PATH_REJECT_DOT_GIT_HFS));
257+
258+
cl_assert_equal_b(false, git_path_isvalid(NULL, "\xe2\x80\xab.\xe2\x80\xacG\xe2\x80\xadI\xe2\x80\xaet", GIT_PATH_REJECT_DOT_GIT_HFS));
259+
cl_assert_equal_b(false, git_path_isvalid(NULL, "\xe2\x81\xab.\xe2\x80\xaaG\xe2\x81\xabI\xe2\x80\xact", GIT_PATH_REJECT_DOT_GIT_HFS));
260+
cl_assert_equal_b(false, git_path_isvalid(NULL, "\xe2\x81\xad.\xe2\x80\xaeG\xef\xbb\xbfIT", GIT_PATH_REJECT_DOT_GIT_HFS));
261+
262+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".", GIT_PATH_REJECT_DOT_GIT_HFS));
263+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".g", GIT_PATH_REJECT_DOT_GIT_HFS));
264+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".gi", GIT_PATH_REJECT_DOT_GIT_HFS));
265+
cl_assert_equal_b(true, git_path_isvalid(NULL, " .git", GIT_PATH_REJECT_DOT_GIT_HFS));
266+
cl_assert_equal_b(true, git_path_isvalid(NULL, "..git\xe2\x80\x8c", GIT_PATH_REJECT_DOT_GIT_HFS));
267+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".gi\xe2\x80\x8dT.", GIT_PATH_REJECT_DOT_GIT_HFS));
268+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".g\xe2\x80It", GIT_PATH_REJECT_DOT_GIT_HFS));
269+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".\xe2gIt", GIT_PATH_REJECT_DOT_GIT_HFS));
270+
cl_assert_equal_b(true, git_path_isvalid(NULL, "\xe2\x80\xaa.gi", GIT_PATH_REJECT_DOT_GIT_HFS));
271+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".gi\x80\x8dT", GIT_PATH_REJECT_DOT_GIT_HFS));
272+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".gi\x8dT", GIT_PATH_REJECT_DOT_GIT_HFS));
273+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".g\xe2i\x80T\x8e", GIT_PATH_REJECT_DOT_GIT_HFS));
274+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".git\xe2\x80\xbf", GIT_PATH_REJECT_DOT_GIT_HFS));
275+
cl_assert_equal_b(true, git_path_isvalid(NULL, ".git\xe2\xab\x81", GIT_PATH_REJECT_DOT_GIT_HFS));
276+
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

tests/resources/nasty/.gitted/objects/15/f7d9f9514eeb65b9588c49b10b1da145a729a2

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
x��1�0 E�s
2+
�H(iں�bae�&q��(5Bܞp������Ժ*�`wژ�ѓ3���C”��1�epB�>����H�KzSKp+R7y�����s]c�M������ӌK���.{W��s�M?P)“�|?�

tests/resources/nasty/.gitted/objects/16/35c47d80914f0abfa43dd4234a948db5bdb107

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
x��=!��9��&����[+/��gc����k��W|/Q��
2+
������gD���d?*k���R�ҋ��+5��wl+�NO8㠿u�[jԩ�)Q������>�Q���/��q��?Pc�=����?q

0 commit comments

Comments
 (0)