diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index 074c5fda0..4536fc574 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -134,51 +134,63 @@ static int ConfigLoad(WOLFSSHD_CONFIG* conf, const char* filename, int depth); static int CountWhitespace(const char* in, int inSz, byte inv); static int SetFileString(char** dst, const char* src, void* heap); -/* convert a string into seconds, handles if 'm' for minutes follows the string - * number, i.e. 2m +/* Largest value GetConfigInt returns, before and after a time multiplier. It + * fits in a long on every target. */ +#define CONFIG_INT_MAX 0x7FFFFFFFL + +/* Convert a decimal string into a number. With isTime, one trailing unit + * suffix (s, m, h, d or w, either case) scales the number to seconds. The whole + * string must be consumed: no sign, whitespace or trailing text. A zero must be + * written as a single '0'. * Returns the value on success and negative value on failure */ static long GetConfigInt(const char* in, int inSz, int isTime, void* heap) { long ret = 0; - int mult = 1; /* multiplier */ + long mult = 1; /* multiplier */ int sz = inSz; + int i; + + (void)heap; if (in == NULL || inSz <= 0) { - ret = WS_BAD_ARGUMENT; + return WS_BAD_ARGUMENT; } /* check for multipliers */ - if (ret == 0 && isTime) { - if (in[sz - 1] == 'm') { - sz--; - mult = 60; - } - else if (in[sz - 1] == 'h') { - sz--; - mult = 60*60; + if (isTime) { + switch (in[sz - 1]) { + case 's': case 'S': mult = 1; sz--; break; + case 'm': case 'M': mult = 60; sz--; break; + case 'h': case 'H': mult = 60*60; sz--; break; + case 'd': case 'D': mult = 60*60*24; sz--; break; + case 'w': case 'W': mult = 60*60*24*7; sz--; break; + default: break; } } - if (ret == 0) { - char* num = (char*)WMALLOC(sz + 1, heap, DYNTYPE_SSHD); - if (num == NULL) { - ret = WS_MEMORY_E; + if (sz <= 0) { + return WS_BAD_ARGUMENT; + } + + for (i = 0; i < sz; i++) { + if (in[i] < '0' || in[i] > '9') { + return WS_BAD_ARGUMENT; } - else { - WMEMCPY(num, in, sz); - num[sz] = '\0'; - ret = atol(num); - if (ret == 0 && WSTRCMP(num, "0") != 0) { - ret = WS_BAD_ARGUMENT; - } - else if (ret > 0) { - ret = ret * mult; - } - WFREE(num, heap, DYNTYPE_SSHD); + if (ret > (CONFIG_INT_MAX - (in[i] - '0')) / 10) { + return WS_BAD_ARGUMENT; } + ret = ret * 10 + (in[i] - '0'); } - return ret; + if (ret == 0 && sz != 1) { + return WS_BAD_ARGUMENT; + } + + if (ret > CONFIG_INT_MAX / mult) { + return WS_BAD_ARGUMENT; + } + + return ret * mult; } /* returns WS_SUCCESS on success, removes trailng newlines */ @@ -462,8 +474,6 @@ void wolfSSHD_ConfigFree(WOLFSSHD_CONFIG* conf) } } -#define MAX_LINE_SIZE 160 - typedef struct { int tag; const char* name; @@ -877,9 +887,9 @@ static int HandleInclude(WOLFSSHD_CONFIG *conf, const char *value, int depth) } } - /* Find wildcards */ + /* Find wildcards, value[0] included */ ptr2 = ptr; - while (ptr2 != value) { + for (;;) { if (*ptr2 == '*') { /* Wildcard found */ found = 1; @@ -893,6 +903,9 @@ static int HandleInclude(WOLFSSHD_CONFIG *conf, const char *value, int depth) * not supported */ break; } + if (ptr2 == value) { + break; + } ptr2--; } ptr = ptr2; @@ -1030,29 +1043,31 @@ static int HandleInclude(WOLFSSHD_CONFIG *conf, const char *value, int depth) fileFilled = i; for (i = 0; ret == WS_SUCCESS && i < fileFilled; i++) { - /* Check if filename prefix matches */ - if (prefixLen > 0) { - if ((int)WSTRLEN(fileNames[i]) <= prefixLen) { - continue; - } - if (WSTRNCMP(fileNames[i], prefix, prefixLen) + int nameLen = (int)WSTRLEN(fileNames[i]); + int postfixLen = (postfix != NULL) ? + (int)WSTRLEN(postfix) : 0; + + /* The '*' may match nothing, but the prefix and + * postfix must not overlap. */ + if (nameLen < prefixLen + postfixLen) { + continue; + } + /* as in glob(3), a leading '.' is only + * matched by a literal '.', never by the '*' */ + if (fileNames[i][0] == '.' && + (prefixLen == 0 || prefix[0] != '.')) { + continue; + } + if (prefixLen > 0 && + WSTRNCMP(fileNames[i], prefix, prefixLen) != 0) { - continue; - } + continue; } - if (postfix) { - /* Skip if file is too short */ - if (WSTRLEN(fileNames[i]) <= WSTRLEN(postfix)) { - continue; - } - if (WSTRNCMP(fileNames[i] + - WSTRLEN(fileNames[i]) - - WSTRLEN(postfix), - postfix, WSTRLEN(postfix)) + if (postfixLen > 0 && + WSTRNCMP(fileNames[i] + nameLen - + postfixLen, postfix, postfixLen) != 0) { - /* Not a match */ - continue; - } + continue; } ret = WSNPRINTF(filepath, PATH_MAX, "%s/%s", path, fileNames[i]); @@ -1130,6 +1145,57 @@ static int HandleChrootDir(WOLFSSHD_CONFIG* conf, const char* value) } +/* Match names are a comma separated list of exact names. OpenSSH pattern + * syntax ('*', '?', '!') and empty list entries are rejected rather than kept + * as literals that could never match. Returns WS_SUCCESS when 'list' is + * usable. */ +static int CheckMatchList(const char* list, int listSz) +{ + int ret = WS_SUCCESS; + int i; + int entrySz = 0; + + for (i = 0; ret == WS_SUCCESS && i < listSz; i++) { + if (list[i] == '*' || list[i] == '?' || list[i] == '!') { + ret = WS_FATAL_ERROR; + } + else if (list[i] == ',') { + if (entrySz == 0) { + ret = WS_FATAL_ERROR; + } + entrySz = 0; + } + else { + entrySz++; + } + } + if (ret == WS_SUCCESS && entrySz == 0) { + ret = WS_FATAL_ERROR; + } + + return ret; +} + +/* returns 1 when 'name' equals one entry of the comma separated 'list' */ +static int MatchList(const char* list, const char* name) +{ + word32 nameSz = (word32)WSTRLEN(name); + const char* entry = list; + const char* end; + + for (;;) { + end = WSTRCHR(entry, ','); + if (end == NULL) { + return WSTRCMP(entry, name) == 0; + } + if ((word32)(end - entry) == nameSz && + WSTRNCMP(entry, name, nameSz) == 0) { + return 1; + } + entry = end + 1; + } +} + /* Parse the value of a Match directive into the user and group applies-to * fields of 'config'. The value is a whitespace separated sequence of * keyword/name pairs, e.g. "User alice Group admins". The token immediately @@ -1199,6 +1265,13 @@ static int ParseMatchCriteria(WOLFSSHD_CONFIG* config, const char* value) (out == &config->usrAppliesTo) ? "User" : "Group"); ret = WS_FATAL_ERROR; } + else if (CheckMatchList(tok, tokSz) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Match %s supports only a comma separated list of " + "exact names", + (out == &config->usrAppliesTo) ? "User" : "Group"); + ret = WS_FATAL_ERROR; + } if (ret == WS_SUCCESS) { /* a repeated keyword replaces the earlier value */ @@ -1306,8 +1379,6 @@ static int HandleMatch(WOLFSSHD_CONFIG** conf, const char* value, int valueSz) ret = ParseMatchCriteria(newConf, value); } - /* @TODO handle , separated user/group list */ - /* on failure free the config that will not be added to the list */ if (ret != WS_SUCCESS && newConf != NULL) { wolfSSHD_ConfigFree(newConf); @@ -1739,7 +1810,7 @@ static int ConfigLoad(WOLFSSHD_CONFIG* conf, const char* filename, int depth) WFILE *f; WOLFSSHD_CONFIG* currentConfig; int ret = WS_SUCCESS; - char buf[MAX_LINE_SIZE]; + char buf[WOLFSSHD_MAX_LINE_SIZE]; const char* current; if (conf == NULL || filename == NULL) @@ -1761,8 +1832,33 @@ static int ConfigLoad(WOLFSSHD_CONFIG* conf, const char* filename, int depth) depth++; currentConfig = conf; - while ((current = XFGETS(buf, MAX_LINE_SIZE, f)) != NULL) { + while ((current = XFGETS(buf, WOLFSSHD_MAX_LINE_SIZE, f)) != NULL) { int currentSz = (int)XSTRLEN(current); + int tooLong = 0; + + /* A full buffer without a newline means the line goes on. Drain the + * rest so it is not parsed as a line of its own. */ + if (currentSz == WOLFSSHD_MAX_LINE_SIZE - 1 && + current[currentSz - 1] != '\n') { + char rest[64]; + + while (XFGETS(rest, (int)sizeof(rest), f) != NULL) { + word32 restSz = (word32)XSTRLEN(rest); + word32 k; + + /* only text past the buffer makes the line too long, + * trailing whitespace and the line ending do not */ + for (k = 0; k < restSz; k++) { + if (rest[k] != ' ' && rest[k] != '\t' && + rest[k] != '\r' && rest[k] != '\n') { + tooLong = 1; + } + } + if (restSz > 0 && rest[restSz - 1] == '\n') { + break; + } + } + } /* remove leading spaces and tabs */ while (currentSz > 0 && @@ -1771,14 +1867,23 @@ static int ConfigLoad(WOLFSSHD_CONFIG* conf, const char* filename, int depth) current = current + 1; } - if (currentSz <= 2) { /* \n or \r\n */ - continue; /* empty line */ - } - if (current[0] == '#') { continue; /* commented out line */ } + /* check before the empty line test, leading whitespace may have + * pushed the text of the line past the buffer */ + if (tooLong) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Config line longer than %d " + "bytes in %s", WOLFSSHD_MAX_LINE_SIZE - 1, filename); + ret = WS_FATAL_ERROR; + break; + } + + if (currentSz <= 2) { /* \n or \r\n */ + continue; /* empty line */ + } + ret = ParseConfigLine(¤tConfig, current, currentSz, depth); if (ret != WS_SUCCESS) { fprintf(stderr, "Unable to parse config line : %s\n", current); @@ -1893,22 +1998,22 @@ static int MatchApplies(const WOLFSSHD_CONFIG* node, const char* usr, matches = 1; if (node->usrAppliesTo != NULL) { - if (usr == NULL || XSTRCMP(node->usrAppliesTo, usr) != 0) { + if (usr == NULL || !MatchList(node->usrAppliesTo, usr)) { matches = 0; } } - /* The group selector matches when it equals any group the user belongs - * to, primary or supplementary, mirroring how OpenSSH evaluates - * 'Match Group'. An empty group list matches no group selector, so - * combined blocks fail closed. */ + /* The group selector matches when a list entry equals any group the + * user belongs to, primary or supplementary, mirroring how OpenSSH + * evaluates 'Match Group'. An empty group list matches no group + * selector, so combined blocks fail closed. */ if (matches && node->groupAppliesTo != NULL) { matches = 0; if (grps != NULL) { for (i = 0; i < grpCount; i++) { if (grps[i] == NULL) continue; - if (XSTRCMP(node->groupAppliesTo, grps[i]) == 0) { + if (MatchList(node->groupAppliesTo, grps[i])) { matches = 1; break; } diff --git a/apps/wolfsshd/configuration.h b/apps/wolfsshd/configuration.h index 4ac1b5a59..514f00223 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -40,6 +40,9 @@ typedef struct WOLFSSHD_CONFIG WOLFSSHD_CONFIG; #define WOLFSSHD_WIN_STORE_CONFIG #endif +/* Longest config line. ConfigLoad() refuses a longer one. */ +#define WOLFSSHD_MAX_LINE_SIZE 1024 + #include "auth.h" /* 0 so that privilege separation is default on after struct memset'd on init */ diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index d8dc67f89..706cd6517 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -305,6 +305,10 @@ static int test_ParseConfigLine(void) {"Port 0", "Port 0", 1}, {"Port NaN", "Port wolfsshd", 1}, {"Port no value", "Port \n", 1}, + {"Port trailing junk", "Port 22abc", 1}, + {"Port leading plus", "Port +22", 1}, + {"Port overflow", "Port 99999999999999999999", 1}, + {"Protocol trailing junk", "Protocol 2x", 1}, /* Whitespace tests. */ {"Extra leading whitespace", "Port 22", 0}, @@ -359,6 +363,22 @@ static int test_ParseConfigLine(void) {"Valid login grace time zero minutes", "LoginGraceTime 0m", 0}, {"Valid login grace time zero hours", "LoginGraceTime 0h", 0}, {"Invalid zero padded login grace time", "LoginGraceTime 00", 1}, + {"Valid login grace time s suffix", "LoginGraceTime 60s", 0}, + {"Valid login grace time days", "LoginGraceTime 1d", 0}, + {"Valid login grace time weeks", "LoginGraceTime 1w", 0}, + {"Valid login grace time upper case", "LoginGraceTime 2M", 0}, + {"Login grace time trailing junk", "LoginGraceTime 2junk", 1}, + {"Login grace time word suffix", "LoginGraceTime 60seconds", 1}, + {"Login grace time two suffixes", "LoginGraceTime 1m1", 1}, + {"Login grace time overflow", "LoginGraceTime 9999999999w", 1}, + /* the limit is CONFIG_INT_MAX seconds, before and after the + * multiplier: 2147483647 / 604800 is 3550, / 60 is 35791394 */ + {"Login grace time max", "LoginGraceTime 2147483647", 0}, + {"Login grace time max plus one", "LoginGraceTime 2147483648", 1}, + {"Login grace time max weeks", "LoginGraceTime 3550w", 0}, + {"Login grace time weeks overflow", "LoginGraceTime 3551w", 1}, + {"Login grace time max minutes", "LoginGraceTime 35791394m", 0}, + {"Login grace time minutes overflow", "LoginGraceTime 35791395m", 1}, /* Permit empty password tests. */ {"Permit empty password no", "PermitEmptyPasswords no", 0}, @@ -397,6 +417,9 @@ static int test_ParseConfigLine(void) {"Include file DNE", "Include sshd_config.d/test-dne.conf", 1}, {"Include wildcard exists", "Include sshd_config.d/*.conf", 0}, {"Include wildcard NDE", "Include sshd_config.d/*.dne", 0}, + /* a leading '*' with no directory part is still a wildcard, so a + * pattern that matches nothing is not opened as a literal file */ + {"Include leading wildcard NDE", "Include *.wolfsshd-dne", 0}, }; const int numVectors = (int)(sizeof(vectors) / sizeof(*vectors)); @@ -454,6 +477,31 @@ static int test_ParseConfigLine(void) Log(" Testing scenario: bare keyword matches, then fails."); ret = CheckScenario(PCL("StrictModes") == WS_BAD_ARGUMENT); } + + /* the time suffixes scale the value to seconds */ + if (ret == WS_SUCCESS) { + static const struct { + const char* line; + long expect; + } times[] = { + { "LoginGraceTime 60s", 60 }, + { "LoginGraceTime 2M", 120 }, + { "LoginGraceTime 1h", 3600 }, + { "LoginGraceTime 1d", 86400 }, + { "LoginGraceTime 1w", 604800 }, + { "LoginGraceTime 3550w", 2147040000L }, + { "LoginGraceTime 35791394m", 2147483640L }, + { "LoginGraceTime 2147483647", 2147483647L }, + }; + + for (i = 0; ret == WS_SUCCESS && + i < (int)(sizeof(times) / sizeof(*times)); i++) { + Log(" Testing scenario: %s is %ld seconds.", times[i].line, + times[i].expect); + ret = CheckScenario(PCL(times[i].line) == WS_SUCCESS && + wolfSSHD_ConfigGetGraceTime(conf) == times[i].expect); + } + } #undef PCL if (conf != NULL) @@ -813,6 +861,20 @@ static int test_MatchUnsupportedSelector(void) * unsupported part must not be silently dropped */ {"Mixed User+Address", "Match User alice Address 10.0.0.0/8", 1}, {"Mixed Group+Host", "Match Group dev Host example.com", 1}, + + /* comma separated lists of exact names are supported */ + {"Match User list", "Match User alice,bob", 0}, + {"Match Group list", "Match Group dev,ops", 0}, + + /* pattern syntax and empty list entries would be kept as literals + * that never match, so they are rejected */ + {"Match User wildcard", "Match User a*", 1}, + {"Match User single char wildcard", "Match User a?c", 1}, + {"Match User negation", "Match User !root", 1}, + {"Match Group negated entry", "Match Group dev,!ops", 1}, + {"Match User empty entry", "Match User a,,b", 1}, + {"Match User leading comma", "Match User ,a", 1}, + {"Match Group trailing comma", "Match Group dev,", 1}, }; const int numVectors = (int)(sizeof(vectors) / sizeof(*vectors)); @@ -975,6 +1037,52 @@ static int test_GetUserConfMatchGroupAnd(void) return ret; } +/* 'Match User a,b' and 'Match Group a,b' apply when any one entry equals the + * user or one of the user's groups. The list itself, or a prefix of an entry, + * is not a match. */ +static int test_GetUserConfMatchCommaList(void) +{ + int ret = WS_SUCCESS; + WOLFSSHD_CONFIG* head; + WOLFSSHD_CONFIG* conf; + const char* grps[2]; + + head = wolfSSHD_ConfigNew(NULL); + if (head == NULL) + ret = WS_MEMORY_E; + conf = head; + +#define PCL(s) ParseConfigLine(&conf, s, (int)WSTRLEN(s), 0) + if (ret == WS_SUCCESS) ret = PCL("ForceCommand internal-sftp"); + if (ret == WS_SUCCESS) ret = PCL("Match User alice,bob"); + if (ret == WS_SUCCESS) ret = PCL("ForceCommand /bin/users"); + if (ret == WS_SUCCESS) ret = PCL("Match Group admins,staff"); + if (ret == WS_SUCCESS) ret = PCL("ForceCommand /bin/groups"); +#undef PCL + + grps[0] = "users"; + grps[1] = "staff"; + + if (ret == WS_SUCCESS) + ret = CheckForcedCmd(head, "alice", grps, 1, "/bin/users"); + if (ret == WS_SUCCESS) + ret = CheckForcedCmd(head, "bob", grps, 1, "/bin/users"); + if (ret == WS_SUCCESS) + ret = CheckForcedCmd(head, "carol", grps, 1, "internal-sftp"); + if (ret == WS_SUCCESS) + ret = CheckForcedCmd(head, "alice,bob", grps, 1, "internal-sftp"); + if (ret == WS_SUCCESS) + ret = CheckForcedCmd(head, "ali", grps, 1, "internal-sftp"); + if (ret == WS_SUCCESS) + ret = CheckForcedCmd(head, "bo", grps, 1, "internal-sftp"); + /* the second group entry, reached through a supplementary group */ + if (ret == WS_SUCCESS) + ret = CheckForcedCmd(head, "carol", grps, 2, "/bin/groups"); + + wolfSSHD_ConfigFree(head); + return ret; +} + /* A 'Match Group' directive must match on any of the user's groups, primary or * supplementary, the way OpenSSH evaluates it. The matcher receives the full * group set as a list, so a group named only in a secondary slot still selects @@ -1298,6 +1406,285 @@ static int WriteConfigFile(const char* path, const char* contents) } +/* Applies 'line' to a fresh config preset to LoginGraceTime 5 and checks the + * resulting grace time against 'expect'. */ +static int CheckIncludeGraceTime(const char* line, long expect) +{ + int ret = WS_SUCCESS; + WOLFSSHD_CONFIG* conf; + + Log(" Testing scenario: %s.", line); + conf = wolfSSHD_ConfigNew(NULL); + if (conf == NULL) { + ret = WS_MEMORY_E; + } + if (ret == WS_SUCCESS) { + ret = ParseConfigLine(&conf, "LoginGraceTime 5", + (int)WSTRLEN("LoginGraceTime 5"), 0); + } + if (ret == WS_SUCCESS) { + ret = ParseConfigLine(&conf, line, (int)WSTRLEN(line), 0); + } + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetGraceTime(conf) != expect) { + ret = WS_FATAL_ERROR; + } + Log("%s", ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + wolfSSHD_ConfigFree(conf); + return ret; +} + +/* A '*' in an Include pattern may match an empty sequence, so 'foo*' and + * '*foo' both pick up a file named exactly 'foo'. The fixed prefix and + * postfix must not overlap, so 'ab*ba' does not pick up 'aba'. As in + * glob(3), a leading '.' in a file name is only matched by a literal '.', so + * '*.bak' skips '.bak' and '.x.bak'. */ +static int test_IncludeWildcardEmptyMatch(void) +{ + int ret; + const char* fooPath = "./sshd_config.d/foo"; + const char* abaPath = "./sshd_config.d/aba"; + const char* dotPath = "./sshd_config.d/.bak"; + const char* dotXPath = "./sshd_config.d/.x.bak"; + + ret = WriteConfigFile(fooPath, "LoginGraceTime 7\n"); + if (ret == WS_SUCCESS) { + ret = WriteConfigFile(abaPath, "LoginGraceTime 9\n"); + } + if (ret == WS_SUCCESS) { + ret = WriteConfigFile(dotPath, "LoginGraceTime 8\n"); + } + if (ret == WS_SUCCESS) { + ret = WriteConfigFile(dotXPath, "LoginGraceTime 6\n"); + } + + if (ret == WS_SUCCESS) + ret = CheckIncludeGraceTime("Include sshd_config.d/foo*", 7); + if (ret == WS_SUCCESS) + ret = CheckIncludeGraceTime("Include sshd_config.d/*foo", 7); + if (ret == WS_SUCCESS) + ret = CheckIncludeGraceTime("Include sshd_config.d/f*o", 7); + if (ret == WS_SUCCESS) + ret = CheckIncludeGraceTime("Include sshd_config.d/ab*ba", 5); + if (ret == WS_SUCCESS) + ret = CheckIncludeGraceTime("Include sshd_config.d/a*a", 9); + if (ret == WS_SUCCESS) + ret = CheckIncludeGraceTime("Include sshd_config.d/*.bak", 5); + if (ret == WS_SUCCESS) + ret = CheckIncludeGraceTime("Include sshd_config.d/.b*", 8); + if (ret == WS_SUCCESS) + ret = CheckIncludeGraceTime("Include sshd_config.d/.*.bak", 6); + + (void)WREMOVE(NULL, fooPath); + (void)WREMOVE(NULL, abaPath); + (void)WREMOVE(NULL, dotPath); + (void)WREMOVE(NULL, dotXPath); + return ret; +} + +/* ConfigLoad reads a line in one buffer of WOLFSSHD_MAX_LINE_SIZE (1024) + * bytes. A longer comment is skipped whole, so its tail is never parsed as a + * directive of its own. A longer directive is an error rather than being cut + * short, also when leading whitespace pushes it past the buffer. Whitespace + * past the buffer does not make a line too long. A line that just fits still + * loads. */ +#define TEST_LINE_MAX 1023 + +/* Writes 'contents' to 'path' and loads it into a new config returned in + * *conf, which the caller frees. Returns the result of the load. */ +static int LoadConfigText(const char* path, const char* contents, + WOLFSSHD_CONFIG** conf) +{ + int ret; + + *conf = NULL; + ret = WriteConfigFile(path, contents); + if (ret == WS_SUCCESS) { + *conf = wolfSSHD_ConfigNew(NULL); + if (*conf == NULL) { + ret = WS_MEMORY_E; + } + } + if (ret == WS_SUCCESS) { + ret = wolfSSHD_ConfigLoad(*conf, path); + } + + return ret; +} + +static int test_ConfigLoadLongLine(void) +{ + int ret = WS_SUCCESS; + WOLFSSHD_CONFIG* conf = NULL; + const char* path = "./long_line.conf"; + const char* fill = "/bin/true"; + char* contents; + word32 i, fillSz, prefixSz; + const char* cmd; + + contents = (char*)WMALLOC(TEST_LINE_MAX * 2 + 64, NULL, DYNTYPE_STRING); + if (contents == NULL) { + return WS_MEMORY_E; + } + + /* a comment that fills the buffer exactly, followed on the same line by + * text that looks like a directive */ + if (ret == WS_SUCCESS) { + Log(" Testing scenario: long comment tail is not parsed."); + WSTRNCPY(contents, "Port 2222\n", TEST_LINE_MAX); + prefixSz = (word32)WSTRLEN(contents); + contents[prefixSz] = '#'; + WMEMSET(contents + prefixSz + 1, 'x', TEST_LINE_MAX - 1); + WMEMCPY(contents + prefixSz + TEST_LINE_MAX, "Port 2223\n", + sizeof("Port 2223\n")); + + ret = WriteConfigFile(path, contents); + if (ret == WS_SUCCESS) { + conf = wolfSSHD_ConfigNew(NULL); + if (conf == NULL) { + ret = WS_MEMORY_E; + } + } + if (ret == WS_SUCCESS) { + ret = wolfSSHD_ConfigLoad(conf, path); + } + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetPort(conf) != 2222) { + ret = WS_FATAL_ERROR; + } + Log("%s", ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + wolfSSHD_ConfigFree(conf); + conf = NULL; + } + + /* a directive one byte too long for the buffer */ + if (ret == WS_SUCCESS) { + Log(" Testing scenario: over-long directive is rejected."); + WSTRNCPY(contents, "ForceCommand ", TEST_LINE_MAX); + prefixSz = (word32)WSTRLEN(contents); + fillSz = (word32)WSTRLEN(fill); + for (i = prefixSz; i < TEST_LINE_MAX + 1; i++) { + contents[i] = ((i - prefixSz) % (fillSz + 1) == fillSz) ? + ' ' : fill[(i - prefixSz) % (fillSz + 1)]; + } + contents[TEST_LINE_MAX + 1] = '\n'; + contents[TEST_LINE_MAX + 2] = '\0'; + + ret = WriteConfigFile(path, contents); + if (ret == WS_SUCCESS) { + conf = wolfSSHD_ConfigNew(NULL); + if (conf == NULL) { + ret = WS_MEMORY_E; + } + } + if (ret == WS_SUCCESS) { + ret = (wolfSSHD_ConfigLoad(conf, path) == WS_SUCCESS) ? + WS_FATAL_ERROR : WS_SUCCESS; + } + Log("%s", ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + wolfSSHD_ConfigFree(conf); + conf = NULL; + } + + /* the same directive at exactly the buffer limit loads whole */ + if (ret == WS_SUCCESS) { + Log(" Testing scenario: directive at the limit loads."); + contents[TEST_LINE_MAX] = '\n'; + contents[TEST_LINE_MAX + 1] = '\0'; + if (contents[TEST_LINE_MAX - 1] == ' ') { + contents[TEST_LINE_MAX - 1] = 'x'; + } + + ret = WriteConfigFile(path, contents); + if (ret == WS_SUCCESS) { + conf = wolfSSHD_ConfigNew(NULL); + if (conf == NULL) { + ret = WS_MEMORY_E; + } + } + if (ret == WS_SUCCESS) { + ret = wolfSSHD_ConfigLoad(conf, path); + } + if (ret == WS_SUCCESS) { + cmd = wolfSSHD_ConfigGetForcedCmd(conf); + if (cmd == NULL || + WSTRLEN(cmd) != (size_t)(TEST_LINE_MAX - prefixSz) || + WSTRNCMP(cmd, contents + prefixSz, + TEST_LINE_MAX - prefixSz) != 0) { + ret = WS_FATAL_ERROR; + } + } + Log("%s", ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + wolfSSHD_ConfigFree(conf); + conf = NULL; + } + + /* the same directive ending in CRLF, at the limit and one byte short of + * it, loads whole. At the limit the buffer leftover is "\r\n"; one short, + * the buffer ends in '\r' and the leftover is "\n". */ + for (i = 0; ret == WS_SUCCESS && i < 2; i++) { + word32 lineSz = TEST_LINE_MAX - i; + + Log(" Testing scenario: CRLF directive %u bytes loads.", lineSz); + contents[lineSz - 1] = 'x'; + contents[lineSz] = '\r'; + contents[lineSz + 1] = '\n'; + contents[lineSz + 2] = '\0'; + + ret = LoadConfigText(path, contents, &conf); + if (ret == WS_SUCCESS) { + cmd = wolfSSHD_ConfigGetForcedCmd(conf); + if (cmd == NULL || + WSTRLEN(cmd) != (size_t)(lineSz - prefixSz) || + WSTRNCMP(cmd, contents + prefixSz, + lineSz - prefixSz) != 0) { + ret = WS_FATAL_ERROR; + } + } + Log("%s", ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + wolfSSHD_ConfigFree(conf); + conf = NULL; + } + + /* a directive pushed past the buffer by leading whitespace is rejected, + * even when what is left in the buffer looks like an empty line */ + for (i = 0; ret == WS_SUCCESS && i < 4; i++) { + static const word32 pads[] = { 1021, 1022, 1023, 1100 }; + + Log(" Testing scenario: %u spaces then a directive is rejected.", + pads[i]); + WMEMSET(contents, ' ', pads[i]); + WMEMCPY(contents + pads[i], "PasswordAuthentication no\n", + sizeof("PasswordAuthentication no\n")); + + ret = (LoadConfigText(path, contents, &conf) == WS_SUCCESS) ? + WS_FATAL_ERROR : WS_SUCCESS; + Log("%s", ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + wolfSSHD_ConfigFree(conf); + conf = NULL; + } + + /* a line of nothing but whitespace is still an empty line, however long */ + if (ret == WS_SUCCESS) { + Log(" Testing scenario: long blank line is skipped."); + WSTRNCPY(contents, "Port 2222\n", TEST_LINE_MAX); + prefixSz = (word32)WSTRLEN(contents); + WMEMSET(contents + prefixSz, ' ', TEST_LINE_MAX + 100); + WMEMCPY(contents + prefixSz + TEST_LINE_MAX + 100, "\n", + sizeof("\n")); + + ret = LoadConfigText(path, contents, &conf); + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetPort(conf) != 2222) { + ret = WS_FATAL_ERROR; + } + Log("%s", ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + wolfSSHD_ConfigFree(conf); + conf = NULL; + } + + (void)WREMOVE(NULL, path); + WFREE(contents, NULL, DYNTYPE_STRING); + return ret; +} + /* Bounded recursion through Include directives: a self-including config * must fail with WS_BAD_ARGUMENT once the depth limit is hit, and the * config object must remain usable so a subsequent load of a normal @@ -7306,11 +7693,14 @@ const TEST_CASE testCases[] = { TEST_DECL(test_GetUserConfMatchRepeatedKeyword), TEST_DECL(test_GetUserConfMatchGroupAnd), TEST_DECL(test_GetUserConfMatchSecondaryGroup), + TEST_DECL(test_GetUserConfMatchCommaList), TEST_DECL(test_CAKeysFileDiffers), TEST_DECL(test_PermitRootLoginModes), TEST_DECL(test_ConfigParseAuthorizedUPNDomains), TEST_DECL(test_MatchUPNToUser), TEST_DECL(test_IncludeRecursionBound), + TEST_DECL(test_IncludeWildcardEmptyMatch), + TEST_DECL(test_ConfigLoadLongLine), TEST_DECL(test_GetUserConfMatchNoInherit), TEST_DECL(test_GetUserConfMatchOverlapCompose), TEST_DECL(test_ConfigIncludeMatchChain),