-
Notifications
You must be signed in to change notification settings - Fork 609
Batch User Model Loading Queries. 32% speedup #2361
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 |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |
| "crypto/sha256" | ||
| "database/sql" | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
| "time" | ||
|
|
@@ -604,15 +605,74 @@ func CountOtherUsers(tx *storage.Connection, id uuid.UUID) (int, error) { | |
| } | ||
|
|
||
| func findUser(tx *storage.Connection, query string, args ...interface{}) (*User, error) { | ||
| obj := &User{} | ||
| if err := tx.Eager().Q().Where(query, args...).First(obj); err != nil { | ||
| // Single query with JSON aggregation - embed by VALUE not pointer | ||
| type userWithJSON struct { | ||
| User | ||
| IdentitiesJSON []byte `db:"identities_json"` | ||
| FactorsJSON []byte `db:"factors_json"` | ||
| } | ||
|
|
||
| var result userWithJSON | ||
|
|
||
| sqlQuery := ` | ||
| select | ||
| u.id, u.aud, u.role, u.email, u.is_sso_user, | ||
| u.encrypted_password, u.email_confirmed_at, u.invited_at, | ||
| u.phone, u.phone_confirmed_at, | ||
| u.confirmation_token, u.confirmation_sent_at, u.confirmed_at, | ||
| u.recovery_token, u.recovery_sent_at, | ||
| u.email_change_token_current, u.email_change_token_new, u.email_change, | ||
| u.email_change_sent_at, u.email_change_confirm_status, | ||
| u.phone_change_token, u.phone_change, u.phone_change_sent_at, | ||
| u.reauthentication_token, u.reauthentication_sent_at, | ||
| u.last_sign_in_at, | ||
| u.raw_app_meta_data, u.raw_user_meta_data, | ||
| u.created_at, u.updated_at, u.banned_until, u.deleted_at, u.is_anonymous, | ||
| u.instance_id, | ||
| coalesce((select json_agg(json_build_object( | ||
| 'identity_id', i.id, | ||
| 'id', i.provider_id, | ||
| 'user_id', i.user_id, | ||
| 'identity_data', i.identity_data, | ||
| 'provider', i.provider, | ||
| 'last_sign_in_at', i.last_sign_in_at, | ||
| 'created_at', i.created_at, | ||
| 'updated_at', i.updated_at, | ||
| 'email', i.email | ||
| )) from ` + Identity{}.TableName() + ` i where i.user_id = u.id), '[]') as identities_json, | ||
| coalesce((select json_agg(json_build_object( | ||
| 'id', f.id, | ||
| 'user_id', f.user_id, | ||
| 'created_at', f.created_at, | ||
| 'updated_at', f.updated_at, | ||
| 'status', f.status, | ||
| 'friendly_name', f.friendly_name, | ||
| 'factor_type', f.factor_type, | ||
| 'secret', f.secret, | ||
| 'phone', f.phone, | ||
| 'last_challenged_at', f.last_challenged_at, | ||
| 'web_authn_credential', f.web_authn_credential | ||
| )) from ` + Factor{}.TableName() + ` f where f.user_id = u.id), '[]') as factors_json | ||
| from ` + User{}.TableName() + ` u | ||
| where ` + query | ||
|
Contributor
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. 🟠 Severity: HIGH SQL Injection Risk via String Concatenation: The 💡 Fix SuggestionSuggestion: This SQL injection risk requires an architectural change to the
The current implementation breaks the parameterization contract by concatenating user-provided strings directly into SQL, even though current callers use hardcoded strings. This creates a dangerous pattern that future developers might misuse.
Contributor
Author
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. yeah, i don't love passing raw sql around, but that seems to be the pattern. any suggestions would be great. This could easily be a pair of uuids instead but I don't want to change the functions interface in this PR |
||
|
|
||
| if err := tx.RawQuery(sqlQuery, args...).First(&result); err != nil { | ||
| if errors.Cause(err) == sql.ErrNoRows { | ||
| return nil, UserNotFoundError{} | ||
| } | ||
| return nil, errors.Wrap(err, "error finding user") | ||
| } | ||
|
|
||
| return obj, nil | ||
| // Deserialize identities and factors from JSON | ||
| if err := json.Unmarshal(result.IdentitiesJSON, &result.User.Identities); err != nil { | ||
| return nil, errors.Wrap(err, "error unmarshaling identities") | ||
| } | ||
|
|
||
| if err := json.Unmarshal(result.FactorsJSON, &result.User.Factors); err != nil { | ||
| return nil, errors.Wrap(err, "error unmarshaling factors") | ||
| } | ||
|
|
||
| return &result.User, nil | ||
| } | ||
|
|
||
| // FindUserByEmailAndAudience finds a user with the matching email and audience. | ||
|
|
||
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.
🔴 Severity: CRITICAL
MFA Secret Exposure - The TOTP
secretfield is included in JSON aggregation, bypassing thejson:"-"protection in the Factor struct. This exposes the shared secret used to generate TOTP codes. The original Eager loading respected this tag and excluded secrets. Remove this line andweb_authn_credential(line 654) to prevent exposure in API responses, logs, or caches.Helpful? Add 👍 / 👎
💡 Fix Suggestion
Suggestion: Remove the 'secret' field (line 651) and 'web_authn_credential' field (line 654) from the JSON aggregation query. These fields are marked with json:"-" tags in the Factor struct to prevent exposure in API responses. Including them in the SQL JSON aggregation bypasses this protection and exposes sensitive MFA secrets that could be logged, cached, or transmitted in API responses. The corrected query should only include non-sensitive Factor fields that are safe to serialize.
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 was already being loaded when we used
*. Its not a change in behavior. I'm not clear on the implications of removing it but feel free