-
Notifications
You must be signed in to change notification settings - Fork 311
HPCC-35282 Cache failed user authentication attempts to reduce AD overhead #20602
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: candidate-9.14.x
Are you sure you want to change the base?
Conversation
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.
Pull Request Overview
This PR implements a cache to prevent repeated LDAP authentication attempts for users with invalid credentials. The cache stores username-password pairs for failed authentication attempts and returns early if the same credentials are retried, reducing unnecessary LDAP server load from brute force or repeated failed login attempts.
Key changes:
- Added an unauthorized user cache that stores failed username/password combinations
- Implemented automatic cache expiration every 5 minutes to prevent unbounded growth
- Modified authentication flow to check the cache before attempting LDAP authentication
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.
Pull Request Overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 5 comments.
| #include <map> | ||
| #include <string> | ||
| #include <set> | ||
| #include <openssl/evp.h> |
Copilot
AI
Nov 4, 2025
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.
The OpenSSL EVP header is included but not used anywhere in the code. This import should be removed unless there are plans to hash passwords before caching them.
| #include <openssl/evp.h> |
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.
Was considering strong hashing of invalid passwords. Currently not doing that. Will remove if we decide plaintext is sufficient.
|
|
||
| static CriticalSection lcCrit; | ||
| static CriticalSection uaCrit; | ||
| std::map<std::string, std::string> unauthorizedUserCache; |
Copilot
AI
Nov 4, 2025
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.
Storing plaintext passwords in the cache poses a significant security risk. If the cache is exposed through a memory dump or debugging tool, passwords would be compromised. Consider hashing passwords with a fast cryptographic hash (e.g., SHA-256 using OpenSSL EVP functions) before storing them in the cache. The codebase already has examples of EVP_sha256() usage in system/security/cryptohelper/digisign.cpp.
| std::map<std::string, std::string> unauthorizedUserCache; | |
| // Store SHA-256 hashes of passwords instead of plaintext |
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.
Currently not storing strong invalid passwords. We only store the most recent invalid password, the cache is cleared every 5 minutes, and the purpose is to quickly fail repeated invalid credentials.
| auto it = unauthorizedUserCache.find(username); | ||
| if (it != unauthorizedUserCache.end()) | ||
| { | ||
| if (it->second == std::string(password)) |
Copilot
AI
Nov 4, 2025
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.
String comparison for passwords is not constant-time and may be vulnerable to timing attacks. While this is a cache check rather than the primary authentication, consider using a constant-time comparison function to avoid potential information leakage.
| if (it->second == std::string(password)) | |
| const std::string &cachedPassword = it->second; | |
| const std::string currentPassword(password); | |
| if (cachedPassword.length() == currentPassword.length() && | |
| CRYPTO_memcmp(cachedPassword.data(), currentPassword.data(), cachedPassword.length()) == 0) |
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.
Probably overkill for the purposes of this change.
| void addUserToUnauthenticatedCache(const char *username, const char *password) | ||
| { | ||
| CriticalBlock block(uaCrit); | ||
| unauthorizedUserCache.emplace(username, password); |
Copilot
AI
Nov 4, 2025
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.
Using emplace() here doesn't update existing entries if the username already exists. If a user fails authentication with password A, then immediately tries password B (which also fails), the cache will retain password A. Use operator[] or insert_or_assign() to ensure the cache stores the most recent failed password attempt.
| unauthorizedUserCache.emplace(username, password); | |
| unauthorizedUserCache.insert_or_assign(username, password); |
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.
When a user that previously failed authorization makes another attempt with a different password, the cached entry is removed and normal processing is done. If the new password is not valid, a new cache entry is created. so, no need to handle changing the password.
| } | ||
| else | ||
| { | ||
| addUserToUnauthenticatedCache(username, password); |
Copilot
AI
Nov 4, 2025
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.
Adding expired passwords to the unauthenticated cache at line 2068 may not be appropriate. Password expiration is a different failure mode from invalid credentials. A user with an expired password has valid credentials but needs to change their password. Caching this prevents them from authenticating after changing their password until the cache expires (up to 5 minutes). Consider only caching AS_INVALID_CREDENTIALS failures.
…rhead Added cache for user attempts that are not authorized. Automatically fail repeated attempts. Signed-Off-By: Kenneth Rowland [email protected]
4b3de35 to
f599959
Compare
jakesmith
left a comment
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.
I think the other routes to denial should also be considered, e.g. AS_PASSWORD_VALID_BUT_EXPIRED, AS_ACCOUNT_LOCKED, etc..
I think a better approach would be to add a temporary account lockout mechanism for any user that fails authentication (for most reasons) multiple times within a short space of time, and then they will be blocked for a defined period of time.
e.g. N requests per minute = blocked for 20 mins.
That would also avoid the caching of stale passwords - which although invalid are still sensitive.
|
I opened https://hpccsystems.atlassian.net/browse/HPCC-35300 to generally handle successive attempts to brute force logging in with different passwords. In some ways it could handle the subject of this PR as well. However, I think handling separately is better since the rejection purpose would differ. On the subject of this PR, I left AS_PASSWORD_VALID_BUT_EXPIRED and AS_ACCOUNT_LOCKED out for the following reasons:
We could hash the saved passwords, but since they are not valid and are stored in memory, I felt it a bit of overkill. Plus, each authenticate attempt would have to go through the hashing process. |
|
Not convinced. A successive failed login attempt if expired, or locked in a short space of time, would still be a DoS type attack, so fall under the remit of HPCC-35300. If the logic is fast failed login attempts lock account for a period, then it covers them all. It's a common approach in general, for wrong passwords to, it either requires an admin to reset, or the the timeout period to lapse. The user should see "temporary locked" or similar. |
|
Btw, re. general DoS prevention, I put this prompt into github.com agent's panel yesterday for kicks: This is the result: jakesmith#156 |
Added cache for user attempts that are not authorized. Automatically fail repeated attempts.
Signed-Off-By: Kenneth Rowland [email protected]
Type of change:
Checklist:
Smoketest:
Testing: