On user profile "account" page, reputation in header is sometimes from other communities
On your user profile page while signed in, go to the "Account" tab (where you change your password). Note the reputation shown in the header -- sometimes it's correct, but sometimes it's a value you have on some other community. It seems to be non-deterministic; I can sit on that page and keep refreshing and I see values, randomly, from several other communities.
I tried this on a community where I can see active flags, wondering if the whole header is losing its scope, but the flag count remains correct.
More discussion in Discord community server starting https://discord.com/channels/709567671087136829/709569625029083147/1347638049722597467.
app/views/layouts/_header.html.erb has, on line 88:
<%= current_user.reputation %>
This is called on every page. What might cause current_user to be scoped wrong?
In app/views/user/_tabs.html.erb, that tab is defined thus:
<%= link_to edit_user_registration_path, class: "tabs--item #{current_page?(edit_user_registration_path) ? 'is-active' : ''}" do %>
Account
<% end %>
The _path implies route magic, but user_registration doesn't occur in routes.rb. The actual page you get to is app/view/user/registrations/edit.html.erb, which calls current_user in the render call for the tabs, the first thing in that file. But by the time we get here the header is already handled, and I'm not sure where else to look for clues besides the layout header file itself (already mentioned).
Poked into this, and it seems to go too deep for me to fix. Though the reputation problem can be fixed with
diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb
index bf3f6c61..c59de9ff 100644
--- a/app/views/layouts/_header.html.erb
+++ b/app/views/layouts/_header.html.erb
@@ -85,7 +85,7 @@ $(() => {
<%= link_to user_path(current_user), class: 'header--item is-complex is-visible-on-mobile',
title: 'Manage profile' do %>
<img alt="user avatar" src="<%= avatar_url(current_user, 40) %>" class="header--item-image avatar-40">
- <span class="<%= SiteSetting['SiteHeaderIsDark'] ? 'has-color-white' : 'has-color-tertiary-600' %>"><%= current_user.reputation %></span>
+ <span class="<%= SiteSetting['SiteHeaderIsDark'] ? 'has-color-white' : 'has-color-tertiary-600' %>"><%= current_user.reputation_on(@community.id) %></span>
<% end %>
<% end %>
<a href="#"
it's User.community_user in its entirety that's bad, along with anything depending on it.
uwsgi-1 | Started GET "/users/edit" for 172.20.0.1 at 2025-03-07 22:45:07 +0000
uwsgi-1 | Processing by Users::RegistrationsController#edit as HTML
uwsgi-1 | User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 ORDER BY `users`.`id` ASC LIMIT 1
uwsgi-1 | ↳ app/helpers/application_helper.rb:175:in `current_user'
# Note: This was tested in community_id = 1, but this query uses community_id=3 for some reason?
uwsgi-1 | CommunityUser Load (0.5ms) SELECT `community_users`.* FROM `community_users` WHERE `community_users`.`user_id` = 2 AND `community_users`.`community_id` = 3 LIMIT 1
uwsgi-1 | ↳ app/helpers/application_helper.rb:176:in `current_user'
uwsgi-1 | Host localhost:3000, community #1 (Dev Community)
uwsgi-1 | User 2 (Zoe) signed in
# This query (and all subsequent (omitted) queries use community_id = 1 as expected
uwsgi-1 | PinnedLink Load (0.5ms) SELECT `pinned_links`.* FROM `pinned_links` WHERE (`pinned_links`.`community_id` = 1 OR `pinned_links`.`community_id` IS NULL) AND `pinned_links`.`active` = TRUE AND (shown_before IS NULL OR shown_before > NOW())
uwsgi-1 | ↳ app/controllers/application_controller.rb:277:in `map'
The consequence is that, aside an incorrect rep display, you can get a mod button even when you're not a mod in a given community, if you are a mod in the community the community_user is sourced from:
The details are sourced from another community. With the reputation fix, the erroneous mod button is still present:
In this case, the test scenario was:
- community_id = 1: rep = 11, not moderator
- [community_id = 2: created incorrectly, and not relevant for the example to work]
- community_id = 3: rep = 1, moderator
Though other setups can be used, as per the Discord discussion, it's unclear whether or not
Reproduction steps
Initially on software.codidact.org proper, I only had one tab open, but I consistently failed to get anything but the incorrect reputation, except for by doing to account, clicking F5, and then clicking account again. This mostly worked, but occasionally failed. Why F5 in particular was needed is beyond me.
On a private instance, however, it failed to trigger, except for a few times by random. By also refreshing a tab I had open to a second community (while logged in as the account I'm trying to edit), I triggered it far more consistently. It's still not 100% of the time, but within a few reloads, the rep and mod status is sourced from a different community. Looks like something with activity in multiple places can set it off, but this is also inconsistent with what I saw on codidact - it shouldn't be set off that consistently when I didn't even have a tab open to another community. (I'm also alone on my network, so no one with my IP had another community open at that time either)
This may or may not suggest that the context is contaminated by any request, which may have security implications. That said, it's weird that it specifically happens on /users/edit, and not in any of the other tabs.
The exact problem
The problem seems to come from RegistrationController. RegistrationController and the layout it seems to generate do not set off set_globals in ApplicationController, which is a requirement for RequestContext to be set correctly. Because it isn't set correctly by the time RegistrationController does its thing, /users/edit occasionally requests the wrong community_user based on what appears to be a basic race condition.
Specifically, community_user.rb:11:
scope :for_context, -> { where(community_id: RequestContext.community_id) }
ends up not getting the right RequestContext, presumably because the old one is still there, because for parts of the request, the context is not set.
I feel like this is a red flag that there may be a vulnerability here too, but I'm not familiar enough with ruby or pentesting to check. All signs seem to point to no, but /shrug. All testing so far has been unable to produce rep or other details from other users.
Unfortunately, this is too complex for me to fix, so leaving my testing here for someone else.
@ArtOfCode- can you assess this? In particular, if we have a potential leak of mod status, that's high priority; if it's "only" a weird rep number in some cases, we want to fix it but it's less urgent. Mainly, that weird rep behavior makes me suspicious that there's something deeper going on here.
@LunarWatcher thanks for your investigation!
Hmm, I think I've seen this issue with Devise before... Users being randomly logged in as other users
Hmm, I think I've seen this issue with Devise before... Users being randomly logged in as other users
And this would (I think?) explain why it happens only on that one tab in the user profile -- the only one that's interacting with credentials, thus devise? Odd that this doesn't involve doing anything with credentials -- it doesn't happen when you try to change your password, just when we load the page where you can change your password.