Skip to content

Commit a67d82d

Browse files
committed
feat: defend against CON device names and more if gitoxide.core.protectWindows is enabled.
Note that trailing `.` are forbidden for some reason, but trailing ` ` (space) is forbidden as it's just ignored when creating directories or files, allowing them to be clobbered and merged silently.
1 parent b6a67d7 commit a67d82d

File tree

2 files changed

+94
-2
lines changed

2 files changed

+94
-2
lines changed

gix-validate/src/path.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ pub mod component {
1313
PathSeparator,
1414
#[error("Window path prefixes are not allowed")]
1515
WindowsPathPrefix,
16+
#[error("Windows device-names may have side-effects and are not allowed")]
17+
WindowsReservedName,
18+
#[error("Trailing spaces or dots and the following characters are forbidden in Windows paths, along with non-printable ones: <>:\"|?*")]
19+
WindowsIllegalCharacter,
1620
#[error("The .git name may never be used")]
1721
DotGitDir,
1822
#[error("The .gitmodules file must not be a symlink")]
@@ -101,6 +105,12 @@ pub fn component(
101105
if is_symlink(mode) && is_dot_ntfs(input, "gitmodules", "gi7eba") {
102106
return Err(component::Error::SymlinkedGitModules);
103107
}
108+
109+
if protect_windows {
110+
if let Some(err) = check_win_devices_and_illegal_characters(input) {
111+
return Err(err);
112+
}
113+
}
104114
}
105115

106116
if !(protect_hfs | protect_ntfs) {
@@ -114,6 +124,44 @@ pub fn component(
114124
Ok(input)
115125
}
116126

127+
fn check_win_devices_and_illegal_characters(input: &BStr) -> Option<component::Error> {
128+
let in3 = input.get(..3)?;
129+
if in3.eq_ignore_ascii_case(b"aux") && is_done_windows(input.get(3..)) {
130+
return Some(component::Error::WindowsReservedName);
131+
}
132+
if in3.eq_ignore_ascii_case(b"nul") && is_done_windows(input.get(3..)) {
133+
return Some(component::Error::WindowsReservedName);
134+
}
135+
if in3.eq_ignore_ascii_case(b"prn") && is_done_windows(input.get(3..)) {
136+
return Some(component::Error::WindowsReservedName);
137+
}
138+
if in3.eq_ignore_ascii_case(b"com")
139+
&& input.get(3).map_or(false, |n| *n >= b'1' && *n <= b'9')
140+
&& is_done_windows(input.get(4..))
141+
{
142+
return Some(component::Error::WindowsReservedName);
143+
}
144+
if in3.eq_ignore_ascii_case(b"lpt")
145+
&& input.get(3).map_or(false, |n| n.is_ascii_digit())
146+
&& is_done_windows(input.get(4..))
147+
{
148+
return Some(component::Error::WindowsReservedName);
149+
}
150+
if in3.eq_ignore_ascii_case(b"con")
151+
&& ((input.get(3..6).map_or(false, |n| n.eq_ignore_ascii_case(b"in$")) && is_done_windows(input.get(6..)))
152+
|| (input.get(3..7).map_or(false, |n| n.eq_ignore_ascii_case(b"out$")) && is_done_windows(input.get(7..))))
153+
{
154+
return Some(component::Error::WindowsReservedName);
155+
}
156+
if input.iter().find(|b| **b < 0x20 || b":<>\"|?*".contains(b)).is_some() {
157+
return Some(component::Error::WindowsIllegalCharacter);
158+
}
159+
if input.ends_with(b".") || input.ends_with(b" ") {
160+
return Some(component::Error::WindowsIllegalCharacter);
161+
}
162+
None
163+
}
164+
117165
fn is_symlink(mode: Option<component::Mode>) -> bool {
118166
mode.map_or(false, |m| m == component::Mode::Symlink)
119167
}
@@ -244,3 +292,10 @@ fn is_done_ntfs(input: Option<&[u8]>) -> bool {
244292
}
245293
true
246294
}
295+
296+
fn is_done_windows(input: Option<&[u8]>) -> bool {
297+
let Some(input) = input else { return true };
298+
let skip = input.bytes().take_while(|b| *b == b' ').count();
299+
let Some(next) = input.get(skip) else { return true };
300+
!(*next != b'.' && *next != b':')
301+
}

gix-validate/tests/path/mod.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ mod component {
6060
mktest!(not_dot_git_shorter_ntfs_8_3_disabled, b"git~1", NO_OPTS);
6161
mktest!(not_dot_git_longer_hfs, ".g\u{200c}itu".as_bytes());
6262
mktest!(not_dot_git_shorter_hfs, ".g\u{200c}i".as_bytes());
63+
mktest!(com_0_lower, b"com0");
64+
mktest!(com_without_number_0_lower, b"comm");
65+
mktest!(conout_without_dollar_with_extension, b"conout.c");
66+
mktest!(conin_without_dollar_with_extension, b"conin.c");
67+
mktest!(conin_without_dollar, b"conin");
68+
mktest!(not_nul, b"null");
6369
mktest!(
6470
not_dot_gitmodules_shorter_hfs,
6571
".gitm\u{200c}odule".as_bytes(),
@@ -158,6 +164,16 @@ mod component {
158164
Symlink,
159165
ALL_OPTS
160166
);
167+
mktest!(
168+
not_gitmodules_trailing_space,
169+
b".gitmodules x ",
170+
Error::WindowsIllegalCharacter
171+
);
172+
mktest!(
173+
not_gitmodules_trailing_stream,
174+
b".gitmodules,:$DATA",
175+
Error::WindowsIllegalCharacter
176+
);
161177
mktest!(path_separator_slash_between, b"a/b", Error::PathSeparator);
162178
mktest!(path_separator_slash_leading, b"/a", Error::PathSeparator);
163179
mktest!(path_separator_slash_trailing, b"a/", Error::PathSeparator);
@@ -167,6 +183,29 @@ mod component {
167183
mktest!(path_separator_backslash_between, b"a\\b", Error::PathSeparator);
168184
mktest!(path_separator_backslash_leading, b"\\a", Error::PathSeparator);
169185
mktest!(path_separator_backslash_trailing, b"a\\", Error::PathSeparator);
186+
mktest!(aux_mixed, b"Aux", Error::WindowsReservedName);
187+
mktest!(aux_with_extension, b"aux.c", Error::WindowsReservedName);
188+
mktest!(com_lower, b"com1", Error::WindowsReservedName);
189+
mktest!(com_upper_with_extension, b"COM9.c", Error::WindowsReservedName);
190+
mktest!(trailing_space, b"win32 ", Error::WindowsIllegalCharacter);
191+
mktest!(trailing_dot, b"win32.", Error::WindowsIllegalCharacter);
192+
mktest!(trailing_dot_dot, b"win32 . .", Error::WindowsIllegalCharacter);
193+
mktest!(colon_inbetween, b"colon:separates", Error::WindowsIllegalCharacter);
194+
mktest!(left_arrow, b"arrow<left", Error::WindowsIllegalCharacter);
195+
mktest!(right_arrow, b"arrow>right", Error::WindowsIllegalCharacter);
196+
mktest!(apostrophe, b"a\"b", Error::WindowsIllegalCharacter);
197+
mktest!(pipe, b"a|b", Error::WindowsIllegalCharacter);
198+
mktest!(questionmark, b"a?b", Error::WindowsIllegalCharacter);
199+
mktest!(asterisk, b"a*b", Error::WindowsIllegalCharacter);
200+
mktest!(lpt_mixed_with_number, b"LPt8", Error::WindowsReservedName);
201+
mktest!(nul_mixed, b"NuL", Error::WindowsReservedName);
202+
mktest!(prn_mixed_with_extension, b"PrN.abc", Error::WindowsReservedName);
203+
mktest!(
204+
conout_mixed_with_extension,
205+
b"ConOut$ .xyz",
206+
Error::WindowsReservedName
207+
);
208+
mktest!(conin_mixed, b"conIn$ ", Error::WindowsReservedName);
170209
mktest!(drive_letters, b"c:", Error::WindowsPathPrefix, ALL_OPTS);
171210
mktest!(
172211
virtual_drive_letters,
@@ -244,7 +283,6 @@ mod component {
244283
"..gitmodules",
245284
"gitmodules",
246285
".gitmodule",
247-
".gitmodules x ",
248286
".gitmodules .x",
249287
"GI7EBA~",
250288
"GI7EBA~0",
@@ -255,7 +293,6 @@ mod component {
255293
"GI7EB~1",
256294
"GI7EB~01",
257295
"GI7EB~1X",
258-
".gitmodules,:$DATA",
259296
] {
260297
gix_validate::path::component(valid.into(), Some(Symlink), ALL_OPTS)
261298
.unwrap_or_else(|_| panic!("{valid:?} should have been valid"));

0 commit comments

Comments
 (0)