-
Notifications
You must be signed in to change notification settings - Fork 902
[PM-24971] Sanitize passkey attestation options from AliExpress #6106
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
[PM-24971] Sanitize passkey attestation options from AliExpress #6106
Conversation
|
Claude finished @SaintPatrck's task —— View job Code Review Complete ✅I've reviewed this PR introducing the What Was Reviewed
SummaryArchitecture & Design: ✅ Excellent
Implementation Quality: ✅ Strong
Previous Review Issues: ✅ All Resolved
Security Considerations: ✅ Good
Issues FoundI've created inline comments for the following:
These are minor issues that don't block the PR but would improve code quality and test accuracy. Recommendations
VerdictApproved with minor suggestions 👍 The PR successfully addresses the AliExpress passkey issue with a clean, targeted workaround. The implementation follows project architecture guidelines, has good test coverage, and all previous review feedback has been incorporated. The issues I noted are minor and don't affect the correctness of the fix. |
6c8a933 to
f919401
Compare
|
Great job! No new security vulnerabilities introduced in this pull request |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6106 +/- ##
==========================================
+ Coverage 84.30% 84.90% +0.59%
==========================================
Files 863 724 -139
Lines 55387 52768 -2619
Branches 7725 7665 -60
==========================================
- Hits 46694 44802 -1892
+ Misses 5981 5277 -704
+ Partials 2712 2689 -23 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This commit introduces a sanitizer for `PasskeyAttestationOptions` to address an issue with the AliExpress Android app. The AliExpress app incorrectly appends a newline character to the `user.id` field when creating a passkey. This malformed request causes the passkey creation to fail. To work around this, a `PasskeyAttestationOptionsSanitizer` is introduced. This sanitizer specifically checks if the request originates from the AliExpress relying party ID (`m.aliexpress.com`) and if the `user.id` ends with a newline. If both conditions are met, it trims the `user.id` before the request is further processed, allowing the passkey registration to succeed. Specific changes include: - Added `PasskeyAttestationOptionsSanitizer` interface and its implementation, `PasskeyAttestationOptionSanitizerImpl`, which contains the specific fix for AliExpress. - Injected and utilized the new sanitizer within `BitwardenCredentialManagerImpl` to clean the `PasskeyAttestationOptions` before they are used to register a FIDO2 credential. - Updated dependency injection in `CredentialProviderModule` to provide the sanitizer. - Added unit tests for the new sanitizer logic.
This commit corrects a "santizer" to "sanitizer" typo in the package name within the `com.x8bit.bitwarden.data.credentials` path. The primary change involves renaming the directory `santizer` to `sanitizer`. All corresponding import statements and package declarations that referenced the misspelled path have been updated accordingly across multiple files.
…ionsSanitizerImpl This commit renames `PasskeyAttestationOptionSanitizerImpl` to `PasskeyAttestationOptionsSanitizerImpl` to align with the plural form used in the `PasskeyAttestationOptionsSanitizer` interface it implements.
This commit refines the workaround for a bug in the AliExpress passkey creation process. The previous implementation incorrectly used `contains` to check the Relying Party (RP) ID, which could lead to false positives if another RP ID happened to contain the AliExpress ID as a substring. This has been corrected to use a strict equality check (`==`) to ensure the workaround is applied only to AliExpress. This change makes the fix more precise and robust.
This commit improves the KDoc comments for the `PasskeyAttestationOptionsSanitizer` interface and its `sanitize` method. The goal is to provide a more detailed and clearer explanation of their purpose, particularly in the context of handling passkey options received from a server. The updated documentation clarifies that sanitization involves ensuring the options conform to expected formats, such as Base64 URL decoding fields and setting defaults, before they are used by the client. It also specifies that the `sanitize` method returns a new, sanitized instance.
This commit updates the `PasskeyAttestationOptionsSanitizer` to only trim trailing newlines from the user id. Previously, it would trim both leading and trailing whitespace, which could incorrectly alter user ids that legitimately start with whitespace characters. The `trim()` function has been replaced with `trimEnd()` to correct this behavior.
f919401 to
dd92be6
Compare
...c/main/kotlin/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerImpl.kt
Outdated
Show resolved
Hide resolved
...lin/com/x8bit/bitwarden/data/credentials/sanitizer/PasskeyAttestationOptionsSanitizerImpl.kt
Outdated
Show resolved
Hide resolved
...c/test/kotlin/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerTest.kt
Show resolved
Hide resolved
.../kotlin/com/x8bit/bitwarden/data/credentials/sanitizer/PasskeyAttestationOptionsSanitizer.kt
Outdated
Show resolved
Hide resolved
Corrected `sanitize` implementation Added test coverage for `sanitize` implementation. Updated documentation to better reflect usage. Only trim newlines from end of options.
...c/main/kotlin/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerImpl.kt
Show resolved
Hide resolved
...lin/com/x8bit/bitwarden/data/credentials/sanitizer/PasskeyAttestationOptionsSanitizerImpl.kt
Show resolved
Hide resolved
...c/test/kotlin/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerTest.kt
Show resolved
Hide resolved
.../kotlin/com/x8bit/bitwarden/data/credentials/sanitizer/PasskeyAttestationOptionsSanitizer.kt
Show resolved
Hide resolved
| val sanitizedOptions = sanitizer.sanitize(options) | ||
|
|
||
| assertEquals("user123", sanitizedOptions.user.id) | ||
| assertNotSame(options, sanitizedOptions) |
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.
Can we do assertNotEquals, or is the intent here to verify that they are not the same instance?
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.
This is intentional to verify a new instance with the correct ID is returned.
...c/main/kotlin/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerImpl.kt
Outdated
Show resolved
Hide resolved
| /** | ||
| * Default implementation of [PasskeyAttestationOptionsSanitizer]. | ||
| */ | ||
| object PasskeyAttestationOptionsSanitizerImpl : PasskeyAttestationOptionsSanitizer { |
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.
👍
| // to the user.id field when creating a passkey. This causes the operation to fail | ||
| // downstream. As a workaround, we detect this specific scenario, trim the newline, and | ||
| // re-serialize the JSON request. | ||
| return if (options.relyingParty.id == ALIEXPRESS_RP_ID && |
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.
Is there any downside to doing it on all relaying parties?
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.
Performance wise, the difference is negligible. I chose this route more to retain data integrity for other RP's, and ease of removal when we no longer need the workaround.
I plan to open a PR that performs validation and sanitizes request properties in the SDK, but I don't want that to hold-up fixing this issue.
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.
Makes snese
| ?.runCatching { json.encodeToString(this) } | ||
| ?.fold( | ||
| onSuccess = { it }, | ||
| onFailure = { null }, |
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.
Do we want to propagate the throwable?
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 don't believe we need to propagate it. We only show a Toast when it fails, but we can certainly log it here.
| ?.fold( | ||
| onSuccess = { it }, | ||
| onFailure = { | ||
| Timber.e(it, "Failed to sanitize passkey attestation options.") |
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.
👍
| null | ||
| }, | ||
| ) | ||
| ?: return Fido2RegisterCredentialResult.Error.InternalError |
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, if sanitization fails, we only log the error and return InternalError. This makes it difficult to diagnose issues. Consider:
- Including the relying party ID in the log message for better debugging
- Creating a more specific error type for sanitization failures
- Or at minimum, logging which sanitizer check failed
Example:
onFailure = {
Timber.e(it, "Failed to sanitize passkey attestation options for RP: ${options.relyingParty.id}")
null
},This would help distinguish sanitization failures from other internal errors when investigating issues.
| } | ||
|
|
||
| @Test | ||
| fun `Sanitization with multiple trailing newlines`() { |
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.
📝 Test Case Clarification: The test name says "multiple trailing newlines" but only tests a single newline.
This test currently has userId = "user123\n" which is the same as the first test case. If you want to test multiple trailing newlines, it should be:
val options = createOptions(rpId = "m.aliexpress.com", userId = "user123\n\n")With trimEnd('\n'), this would remove all trailing newlines, resulting in "user123".
| } | ||
|
|
||
| @Test | ||
| fun `Sanitization with preceding trailing spaces and a newline`() { |
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.
📝 Test Case Issue: Similar to the previous test, this test name says "preceding trailing spaces and a newline" but only tests "user123\n".
If you want to test spaces followed by a newline, the test should be:
val options = createOptions(rpId = "m.aliexpress.com", userId = "user123 \n")Note: With trimEnd('\n'), this would result in "user123 " (spaces preserved), not "user123". If you want to remove both spaces and newlines, you'd need trimEnd() or trim(), but that would change the behavior discussed in earlier reviews.

🎟️ Tracking
PM-24971
#5740
📔 Objective
This commit introduces a sanitizer for
PasskeyAttestationOptionsto address an issue with the AliExpress Android app.The AliExpress app incorrectly appends a newline character to the
user.idfield when creating a passkey. This malformed request causes the passkey creation to fail.To work around this, a
PasskeyAttestationOptionsSanitizeris introduced. This sanitizer specifically checks if the request originates from the AliExpress relying party ID (m.aliexpress.com) and if theuser.idends with a newline. If both conditions are met, it trims theuser.idbefore the request is further processed, allowing the passkey registration to succeed.Specific changes include:
PasskeyAttestationOptionsSanitizerinterface and its implementation,PasskeyAttestationOptionSanitizerImpl, which contains the specific fix for AliExpress.BitwardenCredentialManagerImplto clean thePasskeyAttestationOptionsbefore they are used to register a FIDO2 credential.CredentialProviderModuleto provide the sanitizer.📸 Screenshots
Coming soon!
⏰ Reminders before review
🦮 Reviewer guidelines
:+1:) or similar for great changes:memo:) or ℹ️ (:information_source:) for notes or general info:question:) for questions:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion:art:) for suggestions / improvements:x:) or:warning:) for more significant problems or concerns needing attention:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt:pick:) for minor or nitpick changes