Tighten RSBK package-format validation for review remediation

Reject NUL/control bytes and Windows-hostile names in entry names, relax
the over-broad ".." substring ban to component-only, close the
trailing-garbage gap on empty manifests, and relocate the package
CMake subdirectory to its ladder home.
This commit is contained in:
2026-08-02 07:56:45 -04:00
parent 043558a54d
commit e0b4ec2e21
10 changed files with 161 additions and 56 deletions
+38 -1
View File
@@ -56,6 +56,12 @@ static void testEntryNameAccepts() {
CHECK(isValidEntryName(".hidden")); // a leading dot is a bare name
CHECK(isValidEntryName("a.b.c.wav")); // single dots are fine
CHECK(isValidEntryName(std::string(kMaxEntryNameBytes, 'x'))); // at the cap
// Legal names containing a ".." substring that is not the whole name: a
// name can only ever be one path component (separators are banned), so
// ".." as a component is the only expressible traversal.
CHECK(isValidEntryName("take..final.wav"));
CHECK(isValidEntryName("loop...wav"));
CHECK(isValidEntryName("a..b"));
}
static void testEntryNameRejectsSeparatorsAndDots() {
@@ -64,11 +70,16 @@ static void testEntryNameRejectsSeparatorsAndDots() {
CHECK(!isValidEntryName(".."));
CHECK(!isValidEntryName("..\\evil.wav"));
CHECK(!isValidEntryName("../evil.wav"));
CHECK(!isValidEntryName("a..b.wav")); // any ".." occurrence rejects
CHECK(!isValidEntryName("dir/inner.wav"));
CHECK(!isValidEntryName("dir\\inner.wav"));
CHECK(!isValidEntryName("/rooted.wav"));
CHECK(!isValidEntryName("\\rooted.wav"));
// Embedded NUL: every plausible filesystem call (ofstream, fopen,
// CreateFileW off .c_str()) truncates at it, so two names differing only
// after the NUL would collide on one file.
CHECK(!isValidEntryName(std::string("a\0b.wav", 7)));
// Other control bytes (newline here) are equally hostile to logs/UI.
CHECK(!isValidEntryName("a\nb.wav"));
}
static void testEntryNameRejectsAbsolutePrefixes() {
@@ -79,6 +90,31 @@ static void testEntryNameRejectsAbsolutePrefixes() {
CHECK(!isValidEntryName(std::string(kMaxEntryNameBytes + 1, 'x'))); // over cap
}
static void testEntryNameRejectsWindowsHostileNames() {
// Reserved characters.
CHECK(!isValidEntryName("a*b.wav"));
CHECK(!isValidEntryName("a?b.wav"));
CHECK(!isValidEntryName("a|b.wav"));
CHECK(!isValidEntryName("a<b>.wav"));
CHECK(!isValidEntryName("\"q\".wav"));
// Trailing dot or space (silently stripped at creation on Windows).
CHECK(!isValidEntryName("trailing "));
CHECK(!isValidEntryName("trailing."));
CHECK(!isValidEntryName(" "));
CHECK(!isValidEntryName(" "));
// DOS device names, case-insensitive, with and without an extension.
CHECK(!isValidEntryName("NUL"));
CHECK(!isValidEntryName("CON"));
CHECK(!isValidEntryName("con.wav"));
CHECK(!isValidEntryName("PRN"));
CHECK(!isValidEntryName("AUX"));
CHECK(!isValidEntryName("COM1"));
CHECK(!isValidEntryName("com1.txt"));
CHECK(!isValidEntryName("LPT1"));
// Not a device name: a real filename that merely starts with one.
CHECK(isValidEntryName("console.wav"));
}
int main() {
testClassifyReadable();
testClassifyTooNew();
@@ -86,6 +122,7 @@ int main() {
testEntryNameAccepts();
testEntryNameRejectsSeparatorsAndDots();
testEntryNameRejectsAbsolutePrefixes();
testEntryNameRejectsWindowsHostileNames();
if (g_fail == 0) {
std::printf("package_format_tests: all passed\n");