[management] Validate account ID in URL matches authenticated account#5478
[management] Validate account ID in URL matches authenticated account#5478John-Dixon-IV wants to merge 1 commit intonetbirdio:mainfrom
Conversation
The updateAccount and deleteAccount handlers extracted accountId from the URL parameter without validating it against the authenticated user's account. While the backend ValidateUserPermissions check catches cross-account attempts, the handler should enforce this as defense-in-depth, consistent with every other handler in the codebase. Add explicit validation that the URL accountId matches userAuth.AccountId, returning 403 if they differ.
📝 WalkthroughWalkthroughAdds account ID validation in the accounts handler to enforce that authenticated users can only access and modify their own accounts. When updating or deleting an account, the handler now compares the path parameter account ID against the authenticated account ID, returning a 403 Forbidden error on mismatch. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
management/server/http/handlers/accounts/accounts_handler_test.go (1)
343-370: Consider asserting the delete manager call is never reached on cross-account requests.Status-code assertion is good; adding a “not called” assertion makes the handler-level short-circuit guarantee explicit.
Suggested test hardening
func TestDeleteAccount_CrossAccountForbidden(t *testing.T) { accountID := "test_account" adminUser := types.NewAdminUser("test_user") handler := initAccountsTestData(t, &types.Account{ Id: accountID, Domain: "hotmail.com", Network: types.NewNetwork(), Users: map[string]*types.User{ adminUser.Id: adminUser, }, Settings: &types.Settings{}, }) + deleteCalled := false + mockAM, ok := handler.accountManager.(*mock_server.MockAccountManager) + if !ok { + t.Fatal("expected MockAccountManager") + } + mockAM.DeleteAccountFunc = func(ctx context.Context, accountID, userID string) error { + deleteCalled = true + return nil + } recorder := httptest.NewRecorder() req := httptest.NewRequest(http.MethodDelete, "/api/accounts/different_account_id", nil) req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{ UserId: adminUser.Id, AccountId: accountID, Domain: "hotmail.com", }) router := mux.NewRouter() router.HandleFunc("/api/accounts/{accountId}", handler.deleteAccount).Methods("DELETE") router.ServeHTTP(recorder, req) assert.Equal(t, http.StatusForbidden, recorder.Code, "cross-account delete should be forbidden") + assert.False(t, deleteCalled, "DeleteAccount should not be called for cross-account requests") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@management/server/http/handlers/accounts/accounts_handler_test.go` around lines 343 - 370, The test TestDeleteAccount_CrossAccountForbidden should also assert that the account-deletion manager method is never invoked when requesting a different account; modify the setup returned by initAccountsTestData to inject a mock or spy manager and add an assertion that its DeleteAccount (or equivalent method on the handler's manager) was not called after invoking handler.deleteAccount, keeping the existing HTTP 403 assertion intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@management/server/http/handlers/accounts/accounts_handler_test.go`:
- Around line 343-370: The test TestDeleteAccount_CrossAccountForbidden should
also assert that the account-deletion manager method is never invoked when
requesting a different account; modify the setup returned by
initAccountsTestData to inject a mock or spy manager and add an assertion that
its DeleteAccount (or equivalent method on the handler's manager) was not called
after invoking handler.deleteAccount, keeping the existing HTTP 403 assertion
intact.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
management/server/http/handlers/accounts/accounts_handler.gomanagement/server/http/handlers/accounts/accounts_handler_test.go
|
Hello @John-Dixon-IV, thanks for your PR. The userAuth.AccountId derives from the URL implicitly. So the check implemented here is done in the managers. We are currently moving the permissions validation in the PR #5482 and it will be better to focus the fix if needed after that is merged. |



Summary
updateAccountanddeleteAccounthandlers extractedaccountIdfrom the URL parameter without validating it against the authenticated user's account from the auth context. Every other handler in the codebase (users, groups, routes, peers, dns) usesuserAuth.AccountIdfrom the auth context — these two were the only ones trusting the URL parameter.accountIdmatchesuserAuth.AccountId, returning 403 if they differ.ValidateUserPermissionscheck mitigates single-account exploitation, but handlers should enforce this as defense-in-depth.Test plan
PutAccount with mismatched accountId returns forbidden— URL has different accountId than auth context → 403TestDeleteAccount_CrossAccountForbidden— DELETE with mismatched accountId → 403Summary by CodeRabbit
Bug Fixes
Tests