Skip to content

Back to the catalog

How to Investigate a GitHub Repo's Real History
opensourcemalware.comwriteup

How to Investigate a GitHub Repo's Real History

Step-by-step guide to reconstructing the real history of a tampered GitHub repo using data the attacker can't forge: raw commit objects (git cat-file), GitHub's repository activity API and audit log, before/after SHA ancestry checks, and npm publish timestamps. Worked through a PolinRider case (nestjsx/nest-access-control) where a forged commit was force-pushed to master plus 22 branches in 38 seconds, with a malicious .vscode/tasks.json runOn folderOpen trigger. It also covers safe handling of untrusted clones and how to tell victim accounts from attacker accounts.

TL;DR Use GitHub's activity API, audit logs, SHA ancestry and registry timestamps to expose forged/back-dated git history, shown on a PolinRider force-push case.
Document

What it is

A six-step investigative methodology for proving that a GitHub repo's commit history was forged, built around a real case: the npm package nest-access-control (repo nestjsx/nest-access-control) was infected with PolinRider malware (a DPRK campaign) that force-pushed a tampered commit disguised as a routine chore: add package-lock.json dependency bump.

How it works

The core insight: commit metadata (author/committer name, email, date) is plain text stored inside the commit object and can be freely forged by a script, but several other data sources cannot be altered after the fact:

  • GitHub's push activity log (GET /repos/{owner}/{repo}/activity) records every push with GitHub's own server timestamp, actor, and before/after SHA — independent of anything in the commit itself.
  • Before/after SHA ancestry: on a legitimate push, before is an ancestor of after; on a force-push that rewrites history, it isn't. Check with git merge-base --is-ancestor <before> <after> (exit code 1 = rewritten).
  • npm registry publish timestamps (curl https://registry.npmjs.org/<package> | jq '.time') are set server-side at actual npm publish time and can't be edited retroactively — useful for scoping whether a supply-chain compromise actually reached published packages, not just the repo.
  • Commit signing: in the case study, the real commit was GPG-signed and GitHub-verified ("verified": true); the forged replacement copied the author's name/email but came back "verified": false, "reason": "unsigned" since the attacker didn't have the private signing key.
  • A tell-tale pattern: the same forged tree pushed to many branches within seconds (in the case study, 16 branches in a 38-second window) indicates an automated script, not normal development.
flowchart TD
    A[Commit metadata: author/committer date] -->|forgeable, local clock| X[Cannot trust alone]
    B[GitHub push activity log] -->|server-side timestamp| V[Independent verification]
    C[npm/PyPI publish time object] -->|server-side timestamp| V
    D[GPG signature verification] -->|requires private key| V
    E[Surviving old commit: forks, CI logs, PR timeline] -->|untouched by force-push| V
    V --> F[Reassemble UTC timeline: who, when, which ref]

What to do

  1. git cat-file -p <commit-sha> — compare author vs committer timestamp/timezone for mismatches.
  2. Pull GitHub's activity feed; if you have org admin access, the audit log (git.push events, retained 7 days) gives authenticated-actor ground truth.
  3. git merge-base --is-ancestor <before-sha> <after-sha>; echo $? — confirm history rewrite.
  4. Cross-check against npm/PyPI publish timestamps (not Go/PHP registries, which mirror the source repo).
  5. Check for surviving pre-tampering commits via collaborator forks, CI logs, PR force-push events, or git fetch origin <old-sha> by SHA.
  6. Lay out a four-column timeline (timestamp UTC, actor, ref, event) to spot the pattern.

Safety guidance for handling the untrusted clone: work on a disposable, network-restricted VM, never npm install/build/run the code, don't open it in an editor with auto-run tasks enabled (malicious .vscode/tasks.json with runOn: folderOpen was the actual injection vector here), check .gitmodules before recursive clone, don't follow the repo's own setup instructions, and use scoped/revocable credentials only. Keep git patched — git clone --recursive has a known exploit (CVE-2025-48384).

To distinguish a compromised victim account from an actual attacker: look for a genuine prior track record versus padding/forks only, a sharp break from normal pattern (versus uniformly suspicious activity), account age, follower/following ratios, and breadth of targeting across unrelated repos. Default assumption should be victim, not attacker — the page recommends private maintainer notification over public accusation.

Caveats

The page notes commit-hash mismatches can theoretically expose tampering (git fetch shows "forced update", git status shows divergence), but this only works if someone already holds a pre-rewrite copy and notices the warning — most people run git pull --rebase or git reset --hard origin/main and silently pull in the malicious version anyway. Repos with no other clone holders (personal projects) have no fallback detection at all.

Organisations named here

  • commit backdating
  • force-push
  • git history manipulation
  • temp_auto_push.bat

CVEs named here

  • CVE-2025-48384

Technologies named here

  • git cat-file -p
  • npm package nest-access-control
  • VS Code
  • GitHub organization audit log
  • git merge-base --is-ancestor
  • git
  • git fetch origin <sha>
  • GitHub accounts michaelyali and shekohex
  • git diff, git status
  • npm registry metadata for independent publish timestamps
  • curl
  • jq
  • Postman / Insomnia
  • GitHub commit verification
  • AI coding assistant
  • nestjsx/nest-access-control
  • nest-access-control
  • GitHub REST API - List repository activities endpoint
  • Windows
  • npm registry API
  • GitHub
  • npm

Techniques named here

  • Git history manipulation
  • Decoy binary font files padding the injected payload
  • Victim vs attacker account assessment
  • Typosquat npm packages paired with faked repo history
  • Git commit metadata forgery
  • Force-push history rewrite
  • Mass branch poisoning
  • Auto-executing IDE task injection
  • Malicious decoy file padding
  • Malicious postinstall/preinstall scripts
  • Malicious .gitmodules submodule pointers
  • Onboarding instruction hijack
  • Identity/account spoofing via commit metadata
  • Credential harvesting via environment variables

Threat actors named here

  • DPRK IT workers
  • PolinRider
  • DPRK IT worker scheme
  • DPRK