HPCC-35300 LDAP should handle brute force password hack attempts#21172
HPCC-35300 LDAP should handle brute force password hack attempts#21172kenrowland wants to merge 2 commits intohpcc-systems:candidate-10.2.xfrom
Conversation
🔄 Upmerge Test ResultsStatus: ✅ All branches merged successfully ✅ Successful Branches (1)
|
Added cache of failed user authentication attempts with configurable number of allowed failed attempts and lockout timeout Signed-Off-By: Kenneth Rowland kenneth.rowland@lexisnexisrisk.com
🔄 Upmerge Test ResultsStatus: ✅ All branches merged successfully ✅ Successful Branches (1)
|
| user->setAuthenticateStatus(AS_AUTHENTICATED); | ||
|
|
||
| if (AS_AUTHENTICATED == user->getAuthenticateStatus()) | ||
| if (isCaching) |
There was a problem hiding this comment.
question: Shouldn't this entire if (isCaching) still be inside a check to confirm if (AS_AUTHENTICATED == user->getAuthenticateStatus())? Otherwise, as it is now you could be adding a user to the cache if the call m_ldap_client->authenticate(*user) does not authenticate.
There was a problem hiding this comment.
Looks like an artifact of some Copilot changes. Good find. Updated.
| // This design prevents permanent blocking and allows recovery if the underlying condition is resolved. | ||
| bool isExpired(const FailedAuthEntry &entry, unsigned currentTick) const | ||
| { | ||
| const unsigned elapsedMs = currentTick - entry.firstFailureTick; // unsigned wrap is intentional |
There was a problem hiding this comment.
This is intentional and OK because worst it would do is lockout someone for a bit longer than otherwise?
| { | ||
| if (m_cacheTimeoutSeconds >= (std::numeric_limits<unsigned>::max() / 1000U)) | ||
| return std::numeric_limits<unsigned>::max(); | ||
| return m_cacheTimeoutSeconds * 1000U; |
There was a problem hiding this comment.
This may be pedantic, but the setter in combination with this function does have a chance for a race condition to result in a value being set that would return something out of bounds. Maybe use the critical section or have some validation in the setter to keep it in bounds? This is the only thing I saw that probably really warrants a change.
There was a problem hiding this comment.
Addressed with clamping the value.
| m_cache.begin(), | ||
| m_cache.end(), | ||
| [currentTick](const auto& a, const auto& b) { | ||
| const unsigned ageA = currentTick - a.second.firstFailureTick; |
There was a problem hiding this comment.
This would be a possible simplification, but not a requested change. Since msTick is monotonically increasing (until wraparound) couldn't you just compare the firstFailureTick times to get a relative age ordering (smallest value meaning oldest, ignoring error due to wraparound which I can't think of a way to compensate for anyhow). The math with currentTick gives an absolute age, but you still would have error when the two operands are on either side of a wraparound.
There was a problem hiding this comment.
Good question. We should keep subtraction here: unsigned tick subtraction is the correct wrap-safe pattern for monotonic msTick counters. Directly comparing firstFailureTick/currentTick would be less reliable across counter rollover.
🔄 Upmerge Test ResultsStatus: ✅ All branches merged successfully ✅ Successful Branches (1)
|
|
@ghalliday please merge |
ghalliday
left a comment
There was a problem hiding this comment.
@kenrowland some questions about structure and efficiency. I'm not a fan of header only classes, but this is only used from 1 place .
Actually it isn't - thee are many #includes of ldapsecurity.ipp - including from some headers. It is worth minimizing the impact.
| unsigned maxAllowedEntries = defaultMaxAllowedEntries) | ||
| : m_maxFailedAttempts(maxFailedAttempts), | ||
| // Clamp once at construction so ms conversion cannot overflow. | ||
| m_cacheTimeoutSeconds(std::min(cacheTimeoutSeconds, std::numeric_limits<unsigned>::max() / 1000U)), |
There was a problem hiding this comment.
Similar comment to the other PR - use a much lower threshold than 49 days, otherwise it will never expire.
| // Entry has expired; remove it and allow retries. No need to trim the | ||
| // full cache here — trimming happens on every insert/update in updateUserLockoutStatus. | ||
| m_cache.erase(it); | ||
| trimFailedAuthCache(); |
| if (it != m_cache.end()) | ||
| { | ||
| m_cache.erase(it); | ||
| trimFailedAuthCache(); |
| void setCacheTimeoutSeconds(unsigned v) { m_cacheTimeoutSeconds = std::min(v, std::numeric_limits<unsigned>::max() / 1000U); } | ||
| void setMaxAllowedEntries(unsigned v) { m_maxAllowedEntries = v; } | ||
|
|
||
| unsigned getMaxFailedAttempts() const { return m_maxFailedAttempts; } |
There was a problem hiding this comment.
Are these needed - they're not called.
|
|
||
| void trimFailedAuthCache() | ||
| { | ||
| // Expect lock is held by caller. |
There was a problem hiding this comment.
This could be simplified
- since it is called whenever an entry is added it can only have 1 extra entry - that can be checked by an assert.
- When checking for expired entries, it could also keep a pointer to the oldest non-expired. That could then be removed without a subsequent re-iteration.
| return false; | ||
| } | ||
|
|
||
| bool rc = doUserAuthenticate(user); |
There was a problem hiding this comment.
What happens if the authentication failed because the server was unavailable?
|
|
||
| // Initialize/update the shared failed-auth cache with configured values | ||
| { | ||
| CriticalBlock block(CLdapSecManager::s_failedAuthCacheInitLock); |
There was a problem hiding this comment.
This is breaking the encapsulation. There should probably be a single set function that internally locks the critical section.
| void setMaxFailedAttempts(unsigned v) { m_maxFailedAttempts = v; } | ||
| // Keep timeout-ms conversion safe even if set at runtime. | ||
| void setCacheTimeoutSeconds(unsigned v) { m_cacheTimeoutSeconds = std::min(v, std::numeric_limits<unsigned>::max() / 1000U); } | ||
| void setMaxAllowedEntries(unsigned v) { m_maxAllowedEntries = v; } |
There was a problem hiding this comment.
Theoretically this function and timeout should call trimFailedAuthCache(). This could break my assumption that there is at most one extra. I would resolve by clearing all.
Added cache of failed user authentication attempts with configurable number of allowed failed attempts and lockout timeout
Signed-Off-By: Kenneth Rowland kenneth.rowland@lexisnexisrisk.com
Type of change:
Checklist:
Smoketest:
Testing: