Skip to content

TOCTOU Race Condition in ruleset verification #230

@TomHennen

Description

@TomHennen

We recently received this vulnerability report from François Proulx @fproulx-boostsecurity (VP of Security Research, BoostSecurity.io).

We're making it public here because:

  1. As far as we know no one is using this tool in production.
  2. People will be no worse off than they were without this tool (e.g. this doesn't make anyone more vulnerable).
  3. It will be much easier to discuss and motivate fixes.

What follows if Francois' report...


Vulnerability Summary

The current slsa-source-poc implementation is vulnerable to a Time-of-Check to Time-of-Use (TOCTOU) race condition that allows an attacker to bypass required security controls and obtain a falsified SLSA source attestation.

The vulnerability stems from a flawed assumption about the reliability of the updated_at timestamp in GitHub's Ruleset API. The implementation uses this timestamp to determine if a security control was active when a commit was pushed. However, the parameters within a given ruleset (such as the number of required reviewers) can be tampered with by a repository admin without any change to the ruleset's updated_at timestamp.

This allows a malicious repository administrator (or an attacker in possession of a sufficiently permissioned PAT) to temporarily disable a security control, push a malicious or un-reviewed commit, and then re-enable the control. The sourcetool will later check the updated_at timestamp, in an attempt to detect a policy change, but will incorrectly attest that the commit was part of a push which complied with the security policy.

Attack Scenario

  1. Initial State: A repository is configured with a repository ruleset that requires at least one approving review for all pull requests (required_approving_review_count: 1). The updated_at timestamp for this ruleset is 2025-01-01T12:00:00Z.
  2. Tamper with Required Approvals: An attacker with sufficient permissions modifies the ruleset via the GitHub API (or the Web UI), changing the required_approving_review_count from 1 to 0. Crucially, this modification does not change the ruleset's updated_at timestamp, which remains 2025-01-01T12:00:00Z. You can observe it by comparing the response of https://api.github.com/repos///rulesets/<ruleset_id> when playing with the parameters.
  3. Push Malicious Commit: The attacker now pushes a commit to the protected branch without undergoing any review. The sourcetool's commitActivity function correctly records the push time as 2025-06-27T10:30:00Z.
  4. Restore Control: The attacker immediately changes the ruleset back, restoring required_approving_review_count to 1. The updated_at timestamp still remains 2025-01-01T12:00:00Z.
  5. Falsified Attestation: The SLSA source provenance workflow executes.
    • It fetches the current ruleset, which appears secure (reviews required).
    • It reads the updated_at timestamp as 2025-01-01T12:00:00Z and uses this as the Since value for the REVIEW_ENFORCED control.
    • The implementation compares the commit's push time (2025-06-27T10:30:00Z) with the control's supposed start time (2025-01-01T12:00:00Z).
    • Since the push time is after the updated_at time, the implementation incorrectly concludes that the review control was active when the malicious commit was pushed.
    • It generates a signed VSA that wrongly attests to SLSA_SOURCE_LEVEL_3, giving a false sense of security for the compromised commit.

Vulnerable Code

The flaw is systemic across all functions that determine a control's start time by trusting the ruleset.UpdatedAt.Time. The primary example is computeReviewControl in sourcetool/pkg/ghcontrol/checklevel.go.

// File: sourcetool/pkg/ghcontrol/checklevel.go

func (ghc *GitHubConnection) computeReviewControl(ctx context.Context, rules []*github.PullRequestBranchRule) (*slsa.Control, error) {
	var oldestActive *github.RepositoryRuleset
	for _, rule := range rules {
		if ghc.ruleMeetsRequiresReview(rule) {
			ruleset, _, err := ghc.Client().Repositories.GetRuleset(ctx, ghc.Owner(), ghc.Repo(), rule.RulesetID, false)
			if err != nil {
				return nil, err
			}
			if ruleset.Enforcement == EnforcementActive {
				if oldestActive == nil || oldestActive.UpdatedAt.After(ruleset.UpdatedAt.Time) {
					oldestActive = ruleset
				}
			}
		}
	}

	if oldestActive != nil {
		// VULNERABILITY: oldestActive.UpdatedAt.Time is NOT a reliable
		// indicator of when the rule's *parameters* were last changed.
		return &slsa.Control{Name: slsa.ReviewEnforced, Since: oldestActive.UpdatedAt.Time}, nil
	}

	return nil, nil
}

This same pattern of misplaced trust in UpdatedAt.Time is present in computeContinuityControl, computeTagHygieneControl, and computeRequiredChecks, making the entire verification model unreliable.

Impact

This vulnerability undermines the security guarantees of the SLSA source track proof-of-concept. It allows for the generation of fraudulent provenance.

Recommended Mitigation

Investigate whether the behavior of the updated_at timestamp is an intentional design decision or a bug in the GitHub Ruleset API. If it is a bug, it should be fixed to ensure the timestamp is reliably updated upon any change to a ruleset's parameters.

If the behavior is intentional, an alternative to the time-based policy should be considered. One such alternative is to replace the Since value in the policy with a fingerprint of the expected ruleset configuration. This fingerprint, a cryptographic hash of the canonical JSON representation of the ruleset object, would allow the verifier to confirm that the ruleset active at the time of the push exactly matches the one defined in the policy, preventing any undetected tampering.

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