[Security] ListAllGitspaces Authorization Bypass
Summary
The GET /api/v1/gitspaces endpoint fails to enforce space-level authorization when listing
a user's gitspace configurations. The getAuthorizedSpaces() function calls CheckGitspace()
for each space but always marks the space as authorized regardless of the check result, because
the missing continue in the "no access" branch causes authorizedSpaceIDs[spaceID] = true to
execute unconditionally. An authenticated user who has been removed from a space can still
retrieve their gitspace configurations (including access keys and SSH commands) from that space.
Vulnerability Details
app/api/controller/gitspace/list_all.go, lines 127–145:
func (c *Controller) getAuthorizedSpaces(
ctx context.Context,
session *auth.Session,
spacesMap map[int64]string,
) (map[int64]bool, error) {
var authorizedSpaceIDs = make(map[int64]bool, 0)
for spaceID, spacePath := range spacesMap {
err := apiauth.CheckGitspace(
ctx, c.authorizer, session, spacePath, "", enum.PermissionGitspaceView,
)
if err != nil && !apiauth.IsNoAccess(err) { // line 138
return nil, fmt.Errorf("failed to check gitspace auth for space ID %d: %w", spaceID, err)
}
// BUG: no `continue` here — falls through when err is ErrForbidden/ErrUnauthorized
authorizedSpaceIDs[spaceID] = true // line 142 — executes unconditionally
}
return authorizedSpaceIDs, nil
}apiauth.IsNoAccess(err) returns true when err is ErrForbidden or ErrUnauthorized
(app/api/auth/auth.go:90). When CheckGitspace denies access:
err != nil→ true!apiauth.IsNoAccess(err)→ false (it IS a no-access error)- Combined condition → false → does NOT return an error
- Execution falls through to
authorizedSpaceIDs[spaceID] = true
The space is marked authorized in all cases — authorization check has no effect.
Impact
An authenticated user who has been removed from a Gitness space can issue:
GET /api/v1/gitspaces
Authorization: Bearer <user_token>The response returns their gitspace configurations from all spaces, including spaces they no
longer have gitspace_view permission for. Each returned GitspaceConfig may include:
GitspaceInstance.AccessKey— access credential for connecting to the running environmentGitspaceInstance.SSHCommand— full SSH connection commandGitspaceInstance.URL— accessible endpoint URLGitspaceConfig.SSHTokenIdentifier— reference to stored SSH tokencode_repo_url/code_repo_is_private— private repository detailsspace_path— path of the unauthorized space
If a gitspace instance is still running after the user's space membership is revoked, the
exposed AccessKey may allow the user to maintain access to the development environment
beyond the intended revocation point.
PoC
# Step 1: User registers and is added to a space, creates a gitspace there
# Step 2: Admin removes user from the space (revokes membership)
# Step 3: User calls the listing endpoint with their token
curl -s -H "Authorization: Bearer <USER_TOKEN>" \
https://<gitness-host>/api/v1/gitspaces | jq .
# Expected: empty array or only gitspaces from authorized spaces
# Actual: all gitspace configs including from the revoked space,
# with AccessKey, SSHCommand, and URL populatedSeverity: Medium (CVSS 4.3)
CVSS Vector: AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
CWE: CWE-285 (Improper Authorization)
Affected Version: main branch (commit checked: latest as of 2026-05-19)
Source: harness/harness