La Auditora

Article · 2026-11-06

The $6.3B hack was not a code bug

Ten public repositories with a deployment workflow, measured with the exact commit of each one: 807 findings and 115 with a conjunction of authority. None of the ten returns a clean green, and the article says why that is not 807 vulnerabilities.

securitydevopssupplychainopensource

Where the figures come from: my own measurement, reproducible. Every figure in this article has its commit; the breakdown by code is further down, not in an appendix.


What is confirmed, and what the press is still arguing about

Between October 29 and 30, eight outlets published the same fact: the largest theft in the history of cryptocurrency, between $6.3 and $6.4 billion, with TRM Labs attributing it to North Korea. An Ethereum upgrade, EIP-7702, appears in every piece: it lets an account delegate its signing capability to another contract. With that delegation signed, hot wallets were drained.

So far, the headline. Now the part almost nobody is publishing:

Added up, those three pieces say something uncomfortable: most of the money was lost without the upgrade being involved. If that holds, then EIP-7702 is not the cause. It is the amplifier.

And a fourth piece, from the same day, closes the circle from the other side: The News International headlines that Binance «confirms advanced EIP-7702 exploits and compromised DevOps laptop». That is: that the vector came through a compromised engineer machine is not this article's hypothesis — the exchange itself confirmed it. The fact is not mine; the angle is.

All four headlines were verified on October 30, 2026 with publication date and outlet; none is quoted from memory.

And if it is not the cause, the right question is not «what primitive failed?». It is: who could sign?


The hole is the chain of authority, not the line of code

A code audit looks for the error in the program. The incident points elsewhere: an engineer's machine with signing and deployment capability, and a delegation that turns one signature into permanent authority. None of that lives in a badly written line. It lives in the chain of authority: which identity can publish, with which token, from which artifact, and with how many factors.

That is not a hypothesis about cryptocurrency. It is the structure of any modern deployment. And it can be measured.


We measured it in our own house first

We have an instrument that audits the authority surface of GitHub Actions: who has write permission and at the same time executes code, who can publish through OIDC and at the same time builds the artifact, which actions come in by movable tag, where secrets travel inline.

We ran it against our own repository, before running it against anyone else's.

Result: 8 findings, RC 1 (FINDINGS). Six were from the same workflow. The three that matter:

  1. That workflow declares pages: write + id-token: write at the root, not on the deployment job. Every job inherits it, including the one that compiles.
  2. It comes in through four floating tags (@v4, @v5, @v3, @v4). A tag is a movable reference: whoever can move it executes code inside the perimeter that can publish.
  3. The deploy job consumes the OIDC token without consuming an artifact built separately: it builds and signs in the same context.

None of the three pieces is dramatic on its own. The conjunction is the finding: a publishing token inherited by every job, plus four entry doors that somebody else can move.

Run time: 1.2 seconds. Zero tokens. Without installing anything in the audited repository.


And then we fixed it, because a report with no fix is an invoice

This is where most audits end and where this article should start to differentiate itself.

First move — fix. Root permissions cut to contents: read. pages: write and id-token: write drop to the deploy job, where they are exercised. The workflow splits into two jobs with needs:: one builds and uploads the artifact, the other publishes, and the one that publishes no longer has the repository code on its disk. The four floating refs become full SHAs. And a download-artifact appears that did not exist before: the gate had demanded it from the start and we were right not to believe the first green — without that step, the whole artifact is a sink for the instrument, because no step in the job declares it as an input.

Second move — measure again. 8 findings → 3.

Third move — the one almost nobody makes: do not delete the residue, declare it.

The 3 that remain are not bugs. Two are the corpus keepalive: a job that needs contents: write because its work is to run git push --dry-run. Taking the write away does not close a hole: it turns off the keepalive. The third is the write grant for the deploy job, which now really is where it is used.

The obvious temptation is to silence the instrument. Add an exclusion list, a # noqa, a filter. That turns the audit into theater: the next real finding comes in through the same door we just opened.

What we did instead was an adjudication registry: a file where each exception is written with its code, its workflow, its job and the record that authorizes it. The instrument reads that registry and demands two things a filter cannot give:

Result: RC 0, ADJUDICADO, zero orphans. And when the instrument finds something again that is not in the registry, it goes red again. That is what a # noqa cannot do.

This is the commercial point, and it is worth saying without decoration: the deliverable is not a number of findings. It is a number of findings that moves, and a residue with a name and a record. Anyone can run a scanner and hand you 29 violations. What almost nobody delivers is the proof that the 29 dropped to 3, why those 3 are still there, and who signed each one.


And in somebody else's project, 21 findings

On an active open source project (≈11 workflows), the same instrument returned 21 findings: a release job with contents: write, pull-requests: write and id-token: write that also runs the build in the same context, and a bot token passed inline to a third-party action.

And here comes the part almost no security vendor writes: that job only fires on push to main, inside an if that requires being the repository owner. There is no untrusted input. The finding is structural, not exploitable. Publishing «secret exposed in a public repo» would have been technically true on the surface and false in the risk.

That distinction —structural ≠ exploitable— is the difference between a report a CISO can use and one they have to shut off.

And that foreign repo was not cloned. --repo owner/name resolves the name before the first call, reads the workflows through a read-only API and publishes the exact commit it measured (6c9ca431). Not one write to somebody else's disk, not one extra token. If measuring someone's house requires copying their code, it is not a measurement: it is a move.


Ten repositories, ten measurements, none silent

One case is not a sample. Ten public repositories, all with at least one workflow whose name publishes or deploys something. The inclusion criterion is declared before measuring, not after: a repository enters the batch if it has a deployment or publishing workflow.

Repository Measured commit Findings Conjunction of authority Verdict
withastro/starlight6c9ca431214RC 1
withastro/astro10dae2f35119RC 1
vercel/next.js4fed142e18136RC 1
nuxt/nuxt363f3b875912RC 1
sveltejs/sveltefe21fce0122RC 1
remix-run/remix2eb9249c1709RC 1
angular/angular458303fe264RC 1
vuejs/core24c9d0bf116RC 1
facebook/react16453bcd23411RC 1
vitejs/vite90bc034a4212RC 1
Total10 commits80711510 of 10

Two repositories from the original list were left out, and saying so is part of the method: gatsbyjs/gatsby has no deployment or publishing workflow at all (one only, and it is not a deploy one) — it is declared NO_APLICA and replaced by vitejs/vite. astro-build/astro does not exist: the correct organization is withastro, and withastro/astro was measured.

And here is the part a scanner vendor does not write: 807 are not 807 vulnerabilities. They are 807 observations of the YAML surface. Of those, 115 are the conjunction this article defends — write authority that executes, authority inherited from the root, or missing root permissions. The other 692 split without decoration: 308 movable refs (real hygiene), 153 scope reviews that contract SC-1 marks as REVIEW, 3 root reads under review, and 228 from two codes of the instrument itself that detect the mechanism, not the defect. SECRETO_EXPUESTO flags any ${{ secrets.* }} in an env:, which is the correct way to pass a secret to a step; and SUMIDERO_IMPUDO flags a job with id-token: write that runs actions/checkout, which is what every publishing job has to do to have something to publish. Publishing the raw number would have been the same sin we criticize three paragraphs above, with the sign flipped.

The full table, with the breakdown by code and the exact commit of each measurement, is the one above: ten rows, ten commits, none silent. The batch record keeps the ten SHAs and the criterion they were chosen with.


What this instrument does NOT measure

Saying it is part of the contract, not a clause at the foot of the page:

An instrument that does not declare its blindness is not an instrument: it is an opinion with a JSON format.


The falsifier

This thesis dies in two ways, and both are measurable:

Neither of the two is a defeat. Both force you to stop selling «audit» and start selling «this got closed, and here is the proof».

The falsifier has already been exercised, and the comfortable branch did not come out. Ten repositories, ten measurements: 10 of 10 have at least one conjunction of authority (minimum 2, in sveltejs/svelte; maximum 36, in vercel/next.js). There is not a single repository in the batch where the problem does not exist.

And the measurement found a hole in the falsifier, not in the thesis. The text said «conjunction of inherited authority with movable doors», and that conjunction admits three readings. All three are published:

Reading of the falsifier Repositories that satisfy it
A — at least one conjunction of authority (criterion declared in the evidence)10 of 10
B — conjunction of authority and at least one uses: with a movable tag6 of 10
C — write authority inherited from the root and movable tag4 of 10

No reading gives zero. The first branch of the falsifier —«if it does not find a single one, there is no market»— is refuted under all three. But the second is not reached clean either: only reading A gets to 10 of 10. The falsifier was written with two branches and a hole in the middle, and that hole was filled by the measurement, not by rhetoric. The honest version of the falsifier is not binary: it is a threshold — «if fewer than half of the repositories with deployment show the conjunction, the problem is not structural, and this falls». Measured: 4 of 10 under the strictest reading. It does not fall, and barely.

That resolves the thesis to the second branch under the declared reading, and it has to be said with the same clarity used for our own finding: the conjunction of authority is not a rarity; it is the norm of the ecosystem. If it is the norm, the scan is not a product — anyone runs it in a second, and ours is not billed for speed either. What is scarce is closure: that the number goes down, that the residue has a name and a record, and that the next scan can prove it did not go back up.

The branch that kills the thesis would have been the first. The second came out. The product that survives this measurement is the one already described in the 8 → 3 → 0 section, not the one described in the headline.


If you want to know where your single signing point is

You do not need to install anything or give us access to your code. A repository name is enough: the instrument reads the declared surface of your workflows and returns, in seconds, how many jobs concentrate publishing authority and how many of their inputs somebody else can move.

If the result is «there is no declared chain of authority», that is the finding: absence is published too.


Do you want the measurement on your repository? Leave the name (owner/repo) in the comments of this article and I will publish the count in the same thread: how many jobs concentrate publishing authority, and how many of their inputs somebody else can move. Without cloning your code, without asking you for a key, without installing anything on your machine.

Everything on this page can be measured again.

Every number travels with the command or the file:line that produces it. If something does not add up, the answer is not an argument: it is the instrument, in the reproducible report (in Spanish) and in the repository.