-
Notifications
You must be signed in to change notification settings - Fork 115
wolfsshd: expand AuthorizedKeysFile %u/%h/%% tokens per user #1064
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1900,6 +1900,123 @@ static int test_OpenSecureFile(void) | |
| } | ||
| #endif /* !_WIN32 */ | ||
|
|
||
| /* Verify AuthorizedKeysFile token substitution so an absolute pattern with %u | ||
| * resolves to a per-user path instead of the same literal string for every | ||
| * user. */ | ||
| static int test_ResolveAuthKeysPath(void) | ||
| { | ||
| int ret = WS_SUCCESS; | ||
| int rc; | ||
| word32 i; | ||
| char resolved[MAX_PATH_SZ]; | ||
| char longPat[MAX_PATH_SZ + 16]; | ||
| char longHome[MAX_PATH_SZ]; | ||
| static const struct { | ||
| const char* home; | ||
| const char* pattern; | ||
| const char* user; | ||
| int expectRet; | ||
| const char* expect; | ||
| } vectors[] = { | ||
| /* absolute pattern with %u resolves to a distinct path per user */ | ||
| { "/home/alice", "/etc/ssh/keys/%u", "alice", WS_SUCCESS, | ||
| "/etc/ssh/keys/alice" }, | ||
| { "/home/bob", "/etc/ssh/keys/%u", "bob", WS_SUCCESS, | ||
| "/etc/ssh/keys/bob" }, | ||
| /* %h expands to the home directory */ | ||
| { "/home/alice", "%h/.ssh/authorized_keys", "alice", WS_SUCCESS, | ||
| "/home/alice/.ssh/authorized_keys" }, | ||
| /* %% is a literal percent */ | ||
| { "/home/alice", "/keys/100%%/%u", "alice", WS_SUCCESS, | ||
| "/keys/100%/alice" }, | ||
| /* relative pattern is taken under the home directory */ | ||
| { "/home/alice", "keys/%u", "alice", WS_SUCCESS, | ||
| "/home/alice/keys/alice" }, | ||
| #ifdef _WIN32 | ||
| /* drive-letter and backslash roots are absolute on Windows */ | ||
| { "/home/alice", "C:\\keys\\%u", "alice", WS_SUCCESS, | ||
| "C:\\keys\\alice" }, | ||
| { "/home/alice", "\\keys\\%u", "alice", WS_SUCCESS, | ||
| "\\keys\\alice" }, | ||
| #else | ||
| /* on POSIX they are relative, taken under the home directory */ | ||
| { "/home/alice", "C:\\keys\\%u", "alice", WS_SUCCESS, | ||
| "/home/alice/C:\\keys\\alice" }, | ||
| { "/home/alice", "\\keys\\%u", "alice", WS_SUCCESS, | ||
| "/home/alice/\\keys\\alice" }, | ||
| #endif | ||
| /* NULL pattern falls back to the default location */ | ||
| { "/home/alice", NULL, "alice", WS_SUCCESS, | ||
| "/home/alice/.ssh/authorized_keys" }, | ||
| /* a trailing lone '%' is treated as a literal */ | ||
| { "/home/alice", "/etc/keys/%u%", "alice", WS_SUCCESS, | ||
| "/etc/keys/alice%" }, | ||
| /* unrecognized token fails closed */ | ||
| { "/home/alice", "/etc/keys/%q", "alice", WS_FATAL_ERROR, NULL }, | ||
| /* recognized token with no available value (NULL user) fails closed */ | ||
| { "/home/alice", "/etc/keys/%u", NULL, WS_FATAL_ERROR, NULL }, | ||
| }; | ||
|
|
||
| for (i = 0; ret == WS_SUCCESS && i < sizeof(vectors) / sizeof(vectors[0]); | ||
| i++) { | ||
| Log(" Testing scenario: pattern \"%s\" user \"%s\".", | ||
| vectors[i].pattern != NULL ? vectors[i].pattern : "(null)", | ||
| vectors[i].user != NULL ? vectors[i].user : "(null)"); | ||
| WMEMSET(resolved, 0, sizeof(resolved)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [Medium] New path-resolution tests mask missing string termination The new unit test clears Recommendation: Initialize |
||
| rc = ResolveAuthKeysPath(vectors[i].home, vectors[i].pattern, | ||
| vectors[i].user, resolved); | ||
| if (rc != vectors[i].expectRet) { | ||
| Log(" FAILED (rc=%d).\n", rc); | ||
| ret = WS_FATAL_ERROR; | ||
| } | ||
| else if (vectors[i].expect != NULL && | ||
| WSTRCMP(resolved, vectors[i].expect) != 0) { | ||
| Log(" FAILED (got \"%s\").\n", resolved); | ||
| ret = WS_FATAL_ERROR; | ||
| } | ||
| else { | ||
| Log(" PASSED.\n"); | ||
| } | ||
| } | ||
|
|
||
| /* an expansion that exceeds MAX_PATH_SZ fails closed */ | ||
| if (ret == WS_SUCCESS) { | ||
| Log(" Testing scenario: over-length pattern is rejected."); | ||
| WMEMSET(longPat, 'a', sizeof(longPat) - 1); | ||
| longPat[0] = '/'; | ||
| longPat[sizeof(longPat) - 1] = '\0'; | ||
| WMEMSET(resolved, 0, sizeof(resolved)); | ||
| rc = ResolveAuthKeysPath("/home/alice", longPat, "alice", resolved); | ||
| if (rc == WS_FATAL_ERROR) { | ||
| Log(" PASSED.\n"); | ||
| } | ||
| else { | ||
| Log(" FAILED (rc=%d).\n", rc); | ||
| ret = WS_FATAL_ERROR; | ||
| } | ||
| } | ||
|
|
||
| /* a relative pattern under a long home directory fails closed */ | ||
| if (ret == WS_SUCCESS) { | ||
| Log(" Testing scenario: relative pattern under long home is " | ||
| "rejected."); | ||
| WMEMSET(longHome, 'a', sizeof(longHome) - 1); | ||
| longHome[0] = '/'; | ||
| longHome[sizeof(longHome) - 1] = '\0'; | ||
| WMEMSET(resolved, 0, sizeof(resolved)); | ||
| rc = ResolveAuthKeysPath(longHome, "keys/%u", "alice", resolved); | ||
| if (rc == WS_FATAL_ERROR) { | ||
| Log(" PASSED.\n"); | ||
| } | ||
| else { | ||
| Log(" FAILED (rc=%d).\n", rc); | ||
| ret = WS_FATAL_ERROR; | ||
| } | ||
| } | ||
|
|
||
| return ret; | ||
| } | ||
|
|
||
| const TEST_CASE testCases[] = { | ||
| TEST_DECL(test_ConfigDefaults), | ||
| TEST_DECL(test_ParseConfigLine), | ||
|
|
@@ -1917,6 +2034,7 @@ const TEST_CASE testCases[] = { | |
| TEST_DECL(test_IncludeRecursionBound), | ||
| TEST_DECL(test_GetUserAuthTypes), | ||
| TEST_DECL(test_ConfigSetAuthKeysFile), | ||
| TEST_DECL(test_ResolveAuthKeysPath), | ||
| TEST_DECL(test_ConfigFree), | ||
| #ifndef _WIN32 | ||
| TEST_DECL(test_OpenSecureFile), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 [High] Windows drive-relative AuthorizedKeysFile paths are treated as absolute
🚫 BLOCK
bugThe PR adds Windows absolute-path detection, but it treats any
<letter>:prefix as absolute. On Windows, paths likeC:keys\aliceorC:aliceare drive-relative, not fully qualified. BecauseResolveAuthKeysPath()skips the home-directory join whenIsAbsoluteAuthKeysPath()returns true (copying the path unchanged forSearchForPubKeyto open), anAuthorizedKeysFile C:keys\%upattern is opened relative to the daemon process's current directory on drive C instead of under the user's home directory. Data flow: config/public setter ->ExpandAuthKeysTokensexpands%u->IsAbsoluteAuthKeysPathreturns true solely becausepath[1] == ':'->ResolveAuthKeysPathcopies unchanged ->SearchForPubKeyopens it. Under a misconfigured Windows deployment usingX:relativesyntax, an attacker with write access to the relevant current-directory tree can influence which authorized_keys file is loaded. This is introduced by the new_WIN32branch and is not covered by the new Windows test vectors, which only checkC:\keys\%u. Severity views differ across modes: review rates this High (BLOCK), security rates the practical impact Low (requires Windows, a non-default/mistyped config, and write access to the current-directory tree); the stricter High is kept.Recommendation: Require a slash or backslash after the drive colon for fully qualified Windows paths (
path[1] == ':' && (path[2] == '\\' || path[2] == '/')), and add Windows test vectors forC:%uandC:keys\%uthat expect home-relative resolution (or rejection).