Skip to content

context.own() lets an owned child exceed its parent's entitlement declaration #824

Description

@sroussey

What

Entitlements are computed once, at graph start, over graph.getTasks():

  • TaskGraphRunner.ts:895-909computeGraphEntitlementsGraphEntitlementUtils.ts:59 iterates graph.getTasks() and unions each task's entitlements().

A task created inside execute() via context.own(...) is not in that snapshot — it does not exist yet when the snapshot is taken. And the runtime re-check at TaskGraphRunner.ts:547-558 fires only in runTask, i.e. for graph-scheduled tasks; TaskRunner.ts:1073-1077 states in-source that owned children run via child.run(), which never reaches the enforcer.

So a task that declares nothing can own a child that declares — and performs — anything.

Worked example

FileGrepTask.execute() owns a FetchUrlTask and hands it the caller's URL. Pre-hardening the grep task declared no entitlements at all, so:

  1. the graph snapshot saw an empty entitlement set and the enforcer passed;
  2. the owned FetchUrlTask then classified the URL itself (FetchUrlTask.ts:526-534) and set allowPrivate: true because classifyUrl(input.url).kind === "private";
  3. assertResolvedDestinationDeclared passed, because the child's own runInputData.url is the private URL — the check compares the resolved destination against the child's declaration, and the child declared it.

Net effect: a browser-profile graph reached http://169.254.169.254/latest/meta-data/iam/security-credentials/.

The grep task now declares its owned fetch's entitlements itself (fetchUrlEntitlementsFor + hasDynamicEntitlements), which closes that instance. The framework hole is general: any task can do the same by owning a child, and nothing makes the declaration obligation visible.

Proposed directions

Either:

  1. Refuse an own() whose child declares beyond the parent — compute the child's entitlements at own() time and throw if they are not covered by the parent's declaration. Fail-fast, no enforcer needed, but requires the child's URL/input to be known at own() time (often it is not).
  2. Route owned children through activeEnforcer.checkTask — give context.own() access to the run context's enforcer and check the child before it runs. Catches the late-bound case, at the cost of a check on a hot path.

(1) is cheaper and catches the static case; (2) is the one that actually contains a child whose input is derived inside execute(). They compose.

References

  • packages/task-graph/src/task-graph/TaskGraphRunner.ts:547-558, :895-909
  • packages/task-graph/src/task-graph/GraphEntitlementUtils.ts:59
  • packages/task-graph/src/task/TaskRunner.ts:1073-1077
  • packages/tasks/src/task/FetchUrlTask.ts:526-534

Found while hardening FileGrepTask (PR #821 follow-up).

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