Address security issues in the signup module - #667
Conversation
… that carries AdminPermission (admin rights) in any subfolder of the project, not only at the project root. Co-Authored-By: Claude <noreply@anthropic.com>
* SignUpApiAction and BeginAction now email the owner of an already-registered address (new sendExistingAccountEmail helper) instead of doing nothing. Both signup paths now send an email, so response timing no longer reveals whether an account exists. * The existing account is never modified, and send failures are logged and swallowed so the response stays identical to a new signup. The email points the owner to the "Forgot your password?" reset flow. * Renamed the SignUpApiAction success status from USER_ADDED to SUCCESS, which was misleading in the existing-account case where no user is added. The external signup wiki form must key its success branch off SUCCESS. Co-Authored-By: Claude <noreply@anthropic.com>
…ion status * ChangeGroupsApiAction returns TARGET_NOT_ALLOWED when validateGroupChangeTarget refuses a privileged, site, or cross-project target. NO_PERMISSIONS still covers callers that are ineligible for the transition. * Added SignUpGroupChangeSecurityTest. As a non-admin it plants each dangerous transition rule, then verifies the move is refused and membership unchanged, while a plain same-project move still succeeds. Co-Authored-By: Claude <noreply@anthropic.com>
…e group-change test * SignUpApiAction and BeginAction now run the existing-account and new-account branches through one try/catch, so a mail-send failure returns the same generic error for both. * SignUpGroupChangeSecurityTest now verifies through the auditLog schema that a successful self-service group change adds one "Client API Actions" event and a refused move adds none, comparing counts before and after each move since the event is recorded in the root container that outlives project cleanup. See ai/todos/active/TODO-LK-20260723_signup-security.md Co-Authored-By: Claude <noreply@anthropic.com>
* SignUpApiAction and BeginAction also catch SQLException from the new-account insert, so a DB failure returns the same generic error as any other and cannot reveal whether the account existed. * validateGroupChangeTarget drops the workbook-only container filter and its stale comment. The admin check is unchanged. * RemoveGroupChangeProperty now guards against a null rule map and removes the entry with the correct string key. Group-change audit events log group names instead of bare ids. * SignUpGroupChangeSecurityTest adds a case for a target that inherits admin via membership in an admin group. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens the signup module by preventing privilege escalation through self-service group changes, making signup responses non-enumerable, improving audit logging of admin configuration changes, and ensuring mail-send failures don’t leak internal details to callers.
Changes:
- Adds server-side validation for self-service group transitions to block moves into site groups, cross-project groups, or any group with administrative permission anywhere in the project tree; successful moves are now audited.
- Makes signup responses uniform whether or not the email already has an account (and logs internal mail errors while returning a generic message).
- Adds an integration test (
SignUpGroupChangeSecurityTest) that verifies group-change validation and audit behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| signup/src/org/labkey/signup/SignUpController.java | Adds audit logging, uniform signup behavior, and target validation for self-service group moves. |
| signup/test/src/org/labkey/test/tests/signup/SignUpGroupChangeSecurityTest.java | New test covering group-change security validation and auditing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| String existingRules = m.get(String.valueOf(oldgroup)); | ||
| if(existingRules == null) // no rules configured for this source group - nothing to remove | ||
| return false; |
There was a problem hiding this comment.
The null guard was added only to prevent an NPE when the source group has no rules. The false return matches the branch just below it, where !rules.contains(newgroup) also returns false when the rule isn't present, so this keeps the two "nothing to remove" cases consistent rather than changing pre-existing behavior. The action is only reached from the site-admin Remove link on rules that currently exist, so the "nothing to remove" path is a rare edge such as a stale page or double-submit.
There was a problem hiding this comment.
Returning false without adding to the errors collection may result in a blank 200/success page. Consider calling errors.addErrors() for these two return false; branches if you think it's likely enough to want to help the user.
…dit message, matching the admin-config audit events
| String existingRules = m.get(String.valueOf(oldgroup)); | ||
| if(existingRules == null) // no rules configured for this source group - nothing to remove | ||
| return false; |
There was a problem hiding this comment.
Returning false without adding to the errors collection may result in a blank 200/success page. Consider calling errors.addErrors() for these two return false; branches if you think it's likely enough to want to help the user.
| // Reject a target group with admin permission anywhere in the project. | ||
| for (Container c : ContainerManager.getAllChildren(project)) | ||
| { | ||
| if (c.hasPermission(newgroup, AdminPermission.class)) |
There was a problem hiding this comment.
This protects against admin escalation. Is that enough, or do you also need to check for trying to gain Editor or similar?
There was a problem hiding this comment.
On skyline.ms the target group has read-only access, so anything more than that should not be required.
Fixed in 6aaeeb3. validateGroupChangeTarget now rejects a target that grants write (Insert/Update/Delete) or admin permissions anywhere in the project tree.
| // site, or other-project). Report this with its own status so a refused misconfiguration is | ||
| // distinguishable from an ineligible caller (NO_PERMISSIONS above). The specific reason is | ||
| // logged server-side only, not returned to the caller. | ||
| response.put("status", "TARGET_NOT_ALLOWED"); |
There was a problem hiding this comment.
Reveals the distinction between groups that exist and don't. Probably acceptable if it gives a usability win on the client side.
There was a problem hiding this comment.
There isn't a client-side win, since we would show the same message that is shown for NO_PERMISSIONS. I collapsed the rejection to NO_PERMISSIONS. Fixed in 6aaeeb3
| if (_plantedRules.isEmpty()) | ||
| return; | ||
|
|
||
| ensureSignedInAsPrimaryTestUser(); |
There was a problem hiding this comment.
Normally we'd avoid something like this in an @After because it prevents collecting the right screenshots as artifacts (since it does browser navigation). However, I think it's OK here because this is all API-testing, so there's no browser state worth capturing, right?
There was a problem hiding this comment.
Right. This test is entirely API-driven so there is no browser state worth capturing on failure.
* validateGroupChangeTarget now rejects a target that grants write (Editor/Author/Submitter) or admin access. * Refused moves return the generic NO_PERMISSIONS instead of a distinct TARGET_NOT_ALLOWED, so the response no longer reveals which configured rules are misconfigured (the reason is logged server-side only). * RemoveGroupChangeProperty adds a validation error on each no-op path instead of returning a blank success page. * Added a SignUpGroupChangeSecurityTest case for a write-enabled (Editor) target. Co-Authored-By: Claude <noreply@anthropic.com>
Rationale
This PR addresses the security issues found in the signup module following an internal review. It tightens validation on self-service group changes, makes signup responses consistent, improves auditing of administrative changes, and avoids surfacing internal mail errors to callers.
Security review findings (signup module)
The review flagged six items in the signup module. Four are addressed by this PR; the two rate-limiting items were already handled by the kaptcha work in #638.
Related Pull Requests
Changes
SignUpGroupChangeSecurityTest, covering the group-change validation and its auditing.Deployment note. Two DB-stored wiki pages on the skyline website need updating after deployment: the signup form should branch on
status == 'SUCCESS', and the Skyline-daily register form should handle theTARGET_NOT_ALLOWEDstatus returned by a rejected group change (plus a fallback for any unrecognized status).Co-Authored-By: Claude noreply@anthropic.com