Skip to content

Commit 8c4c433

Browse files
authored
additional validation in the statesman service (#2563)
1 parent 5793087 commit 8c4c433

File tree

3 files changed

+54
-14
lines changed

3 files changed

+54
-14
lines changed

taco/internal/domain/identifier_resolver.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,10 @@ type IdentifierResolver interface {
2424

2525
// ResolveTag resolves a tag identifier to UUID within an organization
2626
ResolveTag(ctx context.Context, identifier, orgID string) (string, error)
27+
28+
// UnitBelongsToOrg validates that a unit UUID belongs to the specified organization.
29+
// Returns true if the unit exists and belongs to the org, false otherwise.
30+
// This is used for security validation when a UUID is provided directly.
31+
UnitBelongsToOrg(ctx context.Context, unitUUID, orgID string) (bool, error)
2732
}
2833

taco/internal/repositories/identifier_resolver.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@ func (r *gormIdentifierResolver) ResolveTag(ctx context.Context, identifier, org
119119
return r.resolveResource(ctx, "tags", identifier, orgID)
120120
}
121121

122+
// UnitBelongsToOrg validates that a unit UUID belongs to the specified organization.
123+
// This is a security check to prevent IDOR attacks where an attacker provides
124+
// a unit UUID from another organization.
125+
func (r *gormIdentifierResolver) UnitBelongsToOrg(ctx context.Context, unitUUID, orgID string) (bool, error) {
126+
if r.db == nil {
127+
return false, fmt.Errorf("database not available")
128+
}
129+
130+
var count int64
131+
err := r.db.WithContext(ctx).
132+
Table("units").
133+
Where("id = ? AND org_id = ?", unitUUID, orgID).
134+
Count(&count).Error
135+
136+
if err != nil {
137+
return false, fmt.Errorf("failed to verify unit ownership: %w", err)
138+
}
139+
140+
return count > 0, nil
141+
}
142+
122143
// resolveResource is the generic resolver for org-scoped resources
123144
func (r *gormIdentifierResolver) resolveResource(ctx context.Context, table, identifier, orgID string) (string, error) {
124145
if r.db == nil {

taco/internal/unit/handler.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,40 +42,54 @@ func NewHandler(store domain.UnitManagement, blobStore storage.UnitStore, rbacMa
4242
}
4343

4444
// resolveUnitIdentifier resolves a unit identifier (name or UUID) to its UUID
45+
// SECURITY: This function validates org ownership for all unit identifiers,
46+
// including direct UUIDs, to prevent IDOR attacks.
4547
func (h *Handler) resolveUnitIdentifier(ctx context.Context, identifier string) (string, error) {
4648
// URL-decode first (Echo params may be URL-encoded)
4749
decoded, err := domain.DecodeURLPath(identifier)
4850
if err != nil {
4951
return "", err
5052
}
51-
53+
5254
normalized := domain.DecodeUnitID(decoded)
53-
54-
// If already a UUID, return as-is
55+
56+
// Get org from context - required for security validation
57+
orgCtx, ok := domain.OrgFromContext(ctx)
58+
if !ok {
59+
return "", fmt.Errorf("organization context required")
60+
}
61+
62+
// If already a UUID, VALIDATE it belongs to the org (SECURITY FIX)
5563
if domain.IsUUID(normalized) {
64+
// If resolver is not available, we cannot validate - fail secure
65+
if h.resolver == nil {
66+
return "", fmt.Errorf("cannot validate unit ownership: resolver not available")
67+
}
68+
69+
belongs, err := h.resolver.UnitBelongsToOrg(ctx, normalized, orgCtx.OrgID)
70+
if err != nil {
71+
return "", fmt.Errorf("failed to verify unit ownership: %w", err)
72+
}
73+
if !belongs {
74+
// Don't reveal that the unit exists in another org
75+
return "", fmt.Errorf("unit not found")
76+
}
5677
return normalized, nil
5778
}
58-
79+
5980
// If resolver is not available, return normalized name (will fail at repository layer)
6081
if h.resolver == nil {
6182
return normalized, nil
6283
}
63-
64-
// Get org from context for resolution
65-
orgCtx, ok := domain.OrgFromContext(ctx)
66-
if !ok {
67-
// No org context, return normalized name
68-
return normalized, nil
69-
}
70-
84+
7185
// Resolve name to UUID using the identifier resolver
72-
// Note: ResolveUnit signature is (ctx, identifier, orgID)
86+
// Note: ResolveUnit already validates org scope for name-based lookups
7387
uuid, err := h.resolver.ResolveUnit(ctx, normalized, orgCtx.OrgID)
7488
if err != nil {
7589
// If resolution fails, return error
7690
return "", err
7791
}
78-
92+
7993
return uuid, nil
8094
}
8195

0 commit comments

Comments
 (0)