Skip to content

fix(api): PostTemplatesTags queries database and begins transaction before team ownership check (tag enumeration oracle) #3573

Description

@chill-czar

Problem

In POST /templates/tags (packages/api/internal/handlers/template_tags.go), the control plane handler initiates a database transaction via a.sqlcDB.WithTx(ctx) and executes client.GetTemplateWithBuildByTag before validating whether the authenticated user's team owns the requested template (if aliasInfo.TeamID != team.ID).

This ordering defect causes two distinct issues:

  1. Tag Enumeration / Information Disclosure Oracle:
    When a user sends POST /templates/tags targeting a template owned by a different team:

    • If the requested tag does not exist on that template: GetTemplateWithBuildByTag returns sql.ErrNoRows, which triggers ErrTemplateNotFound, responding with 404 Not Found ("Template '<target>' with tag '<tag>' not found").
    • If the requested tag exists on that template: the query succeeds, and only afterwards at line 115 is aliasInfo.TeamID != team.ID checked, responding with 403 Forbidden ("You don't have access to sandbox template '%s'").
      An unauthorized tenant can therefore determine whether arbitrary private build tags (e.g., staging, v2.0.0-rc1, hotfix) exist on other teams' templates by probing the endpoint and observing the difference between HTTP 404 and HTTP 403.
  2. Unnecessary Database Load and Transaction Allocation:
    Every unauthorized or invalid tag request against another tenant's template allocates a Postgres transaction connection from the pool (WithTx), acquires read locks, executes queries, and then rolls back upon failure.

Root Cause

In packages/api/internal/handlers/template_tags.go:L75-L122, tag resolution and database transaction creation precede the team ownership authorization check:

// Current flow in template_tags.go:
aliasInfo, err := a.templateCache.ResolveAlias(ctx, templateID)
if err != nil { ... }

// BUG: Transaction started and DB queried BEFORE checking aliasInfo.TeamID == team.ID
txErr := a.sqlcDB.WithTx(ctx).Exec(func(queries *queries.Queries) error {
    build, err := queries.GetTemplateWithBuildByTag(ctx, ...)
    if err != nil {
        return ErrTemplateNotFound // Returns 404 to unauthorized caller if tag missing
    }
    ...
    if aliasInfo.TeamID != team.ID {
        return a.sendAPIStoreError(c, http.StatusForbidden, ...) // Returns 403 only if tag exists
    }
})

In contrast, peer handlers such as DeleteTemplatesTags (template_tags.go:L139) and GetTemplatesTemplateIDTags (template_tags.go:L21) enforce if aliasInfo.TeamID != team.ID immediately after alias resolution:

Handler Authorization Check Timing Tag Enumeration Vulnerable?
GET /templates/{templateID}/tags Immediate after ResolveAlias No
DELETE /templates/tags Immediate after ResolveAlias No
POST /templates/tags (Current) Deferred after GetTemplateWithBuildByTag DB query Yes (404 vs 403 Oracle)
POST /templates/tags (Expected) Immediate after ResolveAlias No (403 Forbidden)

Reproduction Steps

  1. Create a template template-A under team-1 with tag v1.0.0.
  2. Authenticate as an unrelated user team-2.
  3. Send POST /templates/tags targeting template-A with tag: "v9.9.9" (non-existent).
    • Observed: HTTP 404 Not Found ({"code": 404, "message": "Template 'template-A' with tag 'v9.9.9' not found"})
  4. Send POST /templates/tags targeting template-A with tag: "v1.0.0" (existing tag).
    • Observed: HTTP 403 Forbidden ({"code": 403, "message": "You don't have access to sandbox template 'template-A'"})
  5. Expected: Both requests must return HTTP 403 Forbidden without leaking metadata about tag existence.

Technical Context

  • File affected: packages/api/internal/handlers/template_tags.go
  • Subsystem: Control Plane API / Templates & Tags
  • Impact: Medium (Security/Privacy: cross-tenant metadata disclosure & unnecessary database transaction overhead)

Proposed Changes

# Change File(s) Affected Complexity
1 Move if aliasInfo.TeamID != team.ID check immediately after ResolveAlias before WithTx packages/api/internal/handlers/template_tags.go Trivial
2 Add unit test TestPostTemplatesTags_RejectsOtherTeamTemplate verifying 403 Forbidden packages/api/internal/handlers/template_tags_test.go Low

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions