0.28.0 β€” the scan can see an attack that lives in two files

octoscope 0.28.0 is out, and the integrity scan can now see an attack that lives in two files instead of one.

Here's the shape it used to miss. You read a workflow, it triggers on pull_request_target so anyone can start it, and it holds nothing worth stealing. Fine. Except the last line is uses: ./.github/workflows/build.yml with secrets: inherit β€” and that file reads your deploy token. Neither file is a finding on its own. Together they're a stranger's pull request opening a path to your secrets.

Tracing that by hand across a dozen workflows is exactly the kind of tedium nobody actually does, so 0.28.0 does it. The finding now names both halves: the trigger, and the workflow an outsider arrives through β€” you never get told a workflow_call file is fork-triggered without being shown how.

Two more things worth knowing about, both invisible in the files themselves:

A workflow that declares no permissions: doesn't run with none β€” it runs with your repository's default, which an owner can widen to read/write. octoscope now reads that setting and joins it in. And where it can't read it, the report stops claiming the workflow holds nothing, because that unreadable setting is exactly what would decide it.

Findings are also ranked heaviest-first now, so the top line is the one to act on. That one came from a reader in this very thread on the 0.27.0 launch, who noticed a +1 could sit above a +4. πŸ™

The detail I enjoyed getting right: reachability and power travel in opposite directions along a chain. Whatever can start the caller reaches everything it calls, however deep. But power doesn't accumulate β€” GitHub only ever lets a called workflow reduce what it was handed, so a workflow asking for contents: write whose caller passes nothing holds nothing. Getting that backwards would have flagged a pile of perfectly safe repos, and a security tool that cries wolf is one you learn to ignore.

brew upgrade gfazioli/tap/octoscope

Cross-repo chains are still out of scope β€” a call into another repository is reported as an unfollowed chain rather than quietly ignored, but it isn't traced. How do you audit workflow permissions today, if at all? And is the cross-repo case something you'd actually want followed?

octoscope

Site:

Newsletter:

Mastodon:

31 views

Add a comment

Replies

Best

This is the same shape as the thing I keep arguing about with agents, which is that every individual step passes review and the composition is the exploit. Your workflow example is exact: neither file is wrong on its own, and any scanner reading one file at a time will keep saying so forever. Two questions. Does the pair analysis compose past two hops, since the same trick works through a chain of three with each link looking duller than the last. And what does it do about the reverse case, where the two files are individually suspicious and the composition is actually fine, because a scanner that gets that wrong loudly is one people switch off. You shipped 0.27 and 0.28 within days of each other and the second one is a strictly harder problem. Good pace.

Β Thanks for taking the time on this β€” "every individual step passes review and the composition is the exploit" is a sharper framing than anything in my own release notes, and it generalises well past workflows.

It does compose past two hops, because it isn't pair analysis β€” every workflow on a branch goes into one set and reachability propagates to a fixpoint, so the bound is how many files there are, not how deep the chain goes. Your "each link looking duller" point doesn't buy an attacker anything either: for a file whose only trigger is workflow_call I throw away what it claims about itself, since nothing it says is decidable without a caller.

The reverse case is the same rule read backwards, and I'm glad you raised it. Reachability accumulates along a chain, power doesn't β€” GitHub only lets a callee reduce what it was handed, so one asking for contents: write whose caller passes nothing holds nothing, and doesn't score. permissions: {} in the caller is the sharp version: grants nothing, but it is a declaration, so the repository default doesn't apply either.

The axis also has a hard ceiling below the "suspicious" threshold, so capability can't move a verdict on its own however many findings a chain produces β€” that constraint is the only reason I was comfortable letting it see more. Cross-repo calls still aren't traced, though: reported as unfollowed rather than quietly skipped, which is honest but not the same as solved.

And thanks for the kind words on the pace πŸ™‚ The agents version of this is the harder problem afaik, since there isn't even a static set to take a fixpoint over. Would genuinely like to hear how you're approaching it.