Conversation
Co-authored-by: anchapin <6326294+anchapin@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
Reviewer's guide (collapsed on small PRs)Reviewer's GuideAdds a constant dummy bcrypt hash and updates verify_password to always run a bcrypt.checkpw call, even when the stored hash is None, to normalize timing and avoid user enumeration; documents the new mitigation in the Sentinel security notes. Sequence diagram for verify_password timing-attack mitigationsequenceDiagram
actor Attacker
participant API as API_LoginEndpoint
participant Auth as AuthService
participant DB as UserRepository
participant Sec as AuthModule
participant Bcrypt as BcryptLibrary
Attacker->>API: POST /login (username, password)
API->>Auth: authenticate(username, password)
Auth->>DB: get_user_by_username(username)
DB-->>Auth: user or None
Auth->>Sec: verify_password(plain_password, user.hashed_password or None)
alt hashed_password is None (user not found)
Sec->>Bcrypt: checkpw(plain_password, DUMMY_HASH)
Bcrypt-->>Sec: False
Sec-->>Auth: False
else hashed_password is not None (user found)
Sec->>Bcrypt: checkpw(plain_password, hashed_password)
Bcrypt-->>Sec: True or False
Sec-->>Auth: True or False
end
Auth-->>API: authentication result
API-->>Attacker: 200 or 401 with similar timing
Class diagram for updated auth module with DUMMY_HASH and verify_passwordclassDiagram
class AuthModule {
+str BCRYPT_COST
+bytes DUMMY_HASH
+str hash_password(password)
+bool verify_password(plain_password, hashed_password)
}
class BcryptLibrary {
+bytes gensalt(rounds)
+bytes hashpw(password_bytes, salt)
+bool checkpw(password_bytes, hashed_bytes)
}
AuthModule ..> BcryptLibrary : uses
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- Since
hashed_passwordcan now beNone, consider updating the type hint and docstring forverify_passwordto acceptOptional[str]to better reflect the actual behavior. - The dummy bcrypt hash is hardcoded with cost 12, while
hash_passwordusesBCRYPT_COST; aligning the dummy hash cost withBCRYPT_COST(and documenting how to regenerate it) would keep timing characteristics consistent if the cost changes. - The new Sentinel entry replaces all previous security notes in
.jules/sentinel.md; if that history is still useful, consider appending the new section instead of deleting the earlier entries.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Since `hashed_password` can now be `None`, consider updating the type hint and docstring for `verify_password` to accept `Optional[str]` to better reflect the actual behavior.
- The dummy bcrypt hash is hardcoded with cost 12, while `hash_password` uses `BCRYPT_COST`; aligning the dummy hash cost with `BCRYPT_COST` (and documenting how to regenerate it) would keep timing characteristics consistent if the cost changes.
- The new Sentinel entry replaces all previous security notes in `.jules/sentinel.md`; if that history is still useful, consider appending the new section instead of deleting the earlier entries.
## Individual Comments
### Comment 1
<location path="backend/src/security/auth.py" line_range="60-62" />
<code_context>
True if password matches, False otherwise
"""
try:
+ if hashed_password is None:
+ # Perform dummy check to mitigate timing attacks for user enumeration
+ bcrypt.checkpw(plain_password.encode("utf-8"), DUMMY_HASH)
+ return False
+
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Consider normalizing more invalid-hash cases through the dummy check to better equalize timing.
Right now only `hashed_password is None` uses the dummy path. Other invalid values (e.g., empty string, malformed hash) fall through to `bcrypt.checkpw(..., hashed_password.encode(...))`, rely on an exception, and return `False` without a bcrypt call, which can be measurably faster. To better equalize timing, you could extend the dummy branch to cover these invalid formats, or invoke the dummy `bcrypt.checkpw` in the `except` block before returning `False`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if hashed_password is None: | ||
| # Perform dummy check to mitigate timing attacks for user enumeration | ||
| bcrypt.checkpw(plain_password.encode("utf-8"), DUMMY_HASH) |
There was a problem hiding this comment.
🚨 suggestion (security): Consider normalizing more invalid-hash cases through the dummy check to better equalize timing.
Right now only hashed_password is None uses the dummy path. Other invalid values (e.g., empty string, malformed hash) fall through to bcrypt.checkpw(..., hashed_password.encode(...)), rely on an exception, and return False without a bcrypt call, which can be measurably faster. To better equalize timing, you could extend the dummy branch to cover these invalid formats, or invoke the dummy bcrypt.checkpw in the except block before returning False.
There was a problem hiding this comment.
Pull request overview
This PR aims to harden authentication against user-enumeration timing attacks by ensuring verify_password does not return faster when the stored hash is missing (None), and it records the mitigation in the Jules Sentinel log.
Changes:
- Added a module-level
DUMMY_HASHand aNonebranch inverify_passwordthat performs a dummybcrypt.checkpwbefore returningFalse. - Updated
.jules/sentinel.mdwith an entry describing the timing-attack mitigation.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
backend/src/security/auth.py |
Adds a dummy bcrypt verification path when hashed_password is None to reduce timing-based user enumeration signals. |
.jules/sentinel.md |
Documents the timing-attack mitigation (but currently overwrites prior Sentinel content). |
Comments suppressed due to low confidence (1)
backend/src/security/auth.py:55
verify_passwordis annotated ashashed_password: strbut the implementation explicitly supportsNone. Please change the type hint (and docstring) toOptional[str]so callers and type checkers match the real contract.
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify a password against its hash.
Args:
plain_password: Plain text password to verify
hashed_password: Hashed password to check against
| if hashed_password is None: | ||
| # Perform dummy check to mitigate timing attacks for user enumeration | ||
| bcrypt.checkpw(plain_password.encode("utf-8"), DUMMY_HASH) | ||
| return False |
There was a problem hiding this comment.
The dummy-hash mitigation here won’t run for non-existent users if callers short-circuit before calling verify_password (e.g., backend/src/api/auth.py returns 401 when user is missing). To actually normalize timing for user enumeration, consider always calling verify_password (passing None when the user isn’t found) or moving the dummy checkpw into the login flow where the user lookup happens.
| # Dummy hash for timing attack prevention (bcrypt hash of 'dummy_password') | ||
| # Calculated using bcrypt.hashpw(b'dummy_password', bcrypt.gensalt(rounds=12)) | ||
| DUMMY_HASH = b'$2b$12$R.S.YqB461B2O.y0U3L6aOhx0zTzP9gq2i.50vR.H7V22uV0U5u9y' |
There was a problem hiding this comment.
DUMMY_HASH is hardcoded with a cost factor of 12; if BCRYPT_COST changes, response times may diverge again. Add a guard to ensure the dummy hash cost matches BCRYPT_COST, or generate/refresh the dummy hash based on BCRYPT_COST so the timing normalization stays correct over time.
| # Dummy hash for timing attack prevention (bcrypt hash of 'dummy_password') | |
| # Calculated using bcrypt.hashpw(b'dummy_password', bcrypt.gensalt(rounds=12)) | |
| DUMMY_HASH = b'$2b$12$R.S.YqB461B2O.y0U3L6aOhx0zTzP9gq2i.50vR.H7V22uV0U5u9y' | |
| # Dummy hash for timing attack prevention. | |
| # Generate it from BCRYPT_COST so the dummy verification path stays aligned | |
| # with the configured bcrypt work factor over time. | |
| DUMMY_PASSWORD = b"dummy_password" | |
| DUMMY_HASH = bcrypt.hashpw(DUMMY_PASSWORD, bcrypt.gensalt(rounds=BCRYPT_COST)) |
| if hashed_password is None: | ||
| # Perform dummy check to mitigate timing attacks for user enumeration | ||
| bcrypt.checkpw(plain_password.encode("utf-8"), DUMMY_HASH) | ||
| return False |
There was a problem hiding this comment.
New behavior was added for hashed_password is None, but the existing unit tests for security.auth.verify_password don’t cover this branch. Please add a test asserting it returns False and still calls bcrypt.checkpw with the dummy hash when hashed_password is None.
| ## 2025-03-01 - Mitigate user enumeration timing attack in verify_password | ||
| **Vulnerability:** User enumeration timing attack and unhandled NoneType in `verify_password`. | ||
| **Learning:** Returning early or throwing an exception when a user is not found (and hash is `None`) creates a noticeable timing difference compared to a valid login attempt. Attackers can use this to enumerate registered users. | ||
| **Prevention:** Normalize response times by executing a dummy bcrypt hash validation (`bcrypt.checkpw`) against a hardcoded dummy hash when the retrieved user hash is `None`. |
There was a problem hiding this comment.
This change replaces the entire Sentinel knowledge base with a single entry, deleting prior security learnings. If the intent is to record an additional finding, append a new section instead of overwriting the existing content (or move older items to an archive file if size is a concern).
verify_passwordinbackend/src/security/auth.pyto prevent user enumeration timing attacks.hashed_passwordisNone(e.g., when a user is not found), the code now validates the plain password against a dummy hash.PR created automatically by Jules for task 8345439447795597925 started by @anchapin
Summary by Sourcery
Mitigate user enumeration timing attacks and handle missing password hashes consistently in authentication.
Bug Fixes:
hashed_passwordisNoneby safely handling this case.Enhancements: