Pixelfed leaks private posts from other Fediverse instances
When following someone on a different server on the Fediverse, the remote server decides whether you are allowed to do so. This enables features like private accounts. Due to an implementation mistake, Pixelfed ignores this and allows anyone to follow even private accounts on other servers. When a legitimate user from a Pixelfed instance follows you on your locked fediverse account, anyone on that Pixelfed instance can read your private posts. You don’t need to be a Pixelfed user to be affected.
Pixelfed admins should update to v1.12.5 ASAP, but upgrading can be a major hurdle.
Storytime
Two weeks ago, a partner and I were on vacation far away from home. We were enjoying some well deserved rest after a long day outside, when I looked over to see her shooting confused looks at her phone.
“Someone favorited a toot that they should not be able to see. How is this possible?” she said, holding it up in my face.
“What? No way.”
But sure enough, the toot was followers only and the person that had liked it was not following her Mastodon account. When I took a look at the other persons profile on pixelfed.social, I noticed that the instance was nevertheless claiming the account was following her. I already dreaded what I felt was about to happen.
I created an account on pixelfed.social and clicked follow on my partner’s Mastodon account, and… I could see all of her private posts. Instead of telling me I’d have to wait to have my follow accepted, I was already following her.
“Oh no, not again”, I said, dreading the thought of spending the next few hours reading PHP code and writing a report.
ActivityPub
In order to understand how this happened we have to know some bits about ActivityPub, the protocol that’s powering the Fediverse.
In most ActivityPub implementations the user can decide whether they want to manually review new followers. In Mastodon this is achieved by disabling “Automatically accept new followers”. Sometimes it’s called Locked Account. Now the server adds the property as:manuallyApprovesFollowers to the profile and all other servers know to display the small lock 🔒 symbol next to your username.
When you click follow on someones profile, your server sends a message to the remote server, requesting a follow. Then the remote server either accepts the request, declines it, or ignores it until eternity, depending on the target users choice.
What’s important to add is, the two-way handshake happens independently of whether the other person has their account locked or not. It’s always up to the remote server to decide if that follow is going to happen. Spec-brained people would phrase it like that:
The side effect of receiving this [follow request] in an outbox is that the server SHOULD add the object to the actor’s following Collection when and only if an Accept activity is subsequently received with this Follow activity as its object.
(From ActivityPub - 6.5 Follow Activity)
The vulnerability
Pixelfeds ActivityPub implementation was (and still is) incomplete. They did export the as:manuallyApprovesFollowers
property, but they didn’t import them for remote accounts. Hence remote accounts always displayed as Not Locked. While being annoying for users, this is not not a security issue in itself.
However, there was another issue. When pixelfed assumes that an account is not locked, it immediately treats a follow attempt as completed. For the server on the other end it looks like a normal follow request. It could be rejected, and pixelfed would still be convinced that a follow relation exists.
This is the responsible code:
1if ($private == true) { 2 $follow = FollowRequest::firstOrCreate([ 3 'follower_id' => $user->profile_id, 4 'following_id' => $target->id, 5 ]); 6 if ($remote == true && config('federation.activitypub.remoteFollow') == true) { 7 (new FollowerController)->sendFollow($user->profile, $target); 8 } 9} else { 10 $follower = Follower::firstOrCreate([ 11 'profile_id' => $user->profile_id, 12 'following_id' => $target->id, 13 ]); 14 15 if ($remote == true && config('federation.activitypub.remoteFollow') == true) { 16 (new FollowerController)->sendFollow($user->profile, $target); 17 } 18 FollowPipeline::dispatch($follower)->onQueue('high'); 19}
app/Http/Controllers/Api/ApiV1Controller.php Lines 859-877
Importantly, your Mastodon or GoToSocial instance isn’t handing your private posts to any random server, just because it asks. The problem only becomes apparent when you have at least one legit accepted follower from a Pixelfed server. Now that server is allowed to fetch all your private posts. And when it knows the posts, it has to decide who to show them. When you accept a follower, you not only place your trust to keep a secret on them, but also on their admin and the software they are running.
Why is this a serious issue?
There’s a reasonable chance you have a follower on one of the big Pixelfed instances, like pixelfed.social. An attacker who wants to see your private posts could create an account there, follow you, and immediately see your posts. You would receive a follow request, but it wouldn’t matter whether you’re accepting it or not.. You can’t possibly notice that your posts are exposed to strangers, and there’s nothing you could do about it.1
The problem is exaggerated by the fact that pixelfeds users are concentrated on few very big instances.
Report
Noticing that seemingly noone had noticed this before, I knew that handling this knowlege responsible and carefully was very important. To follow best practises for a responsible disclosure process I pieced together a detailed analysis along with the request to agree on a disclosure timeline. This was shared privately using GitHub’s Advisory feature alongside an email to their contact address.
Among the things I wrote:
The attack vector becoming public would put many thousand users at danger. Therefore it must be handled with utmost care.
From what I can tell this is a serious issue with severe real-world implications. Please don’t publish the advisory or fixes without further coordination.
Reaction
After about 30 hours I got a first reply from Dansup, the solo pixelfed maintainer.
Hi Thank you for the report, I am working on the fix.
Another two days later he pushed the fixes into the public repository.
By doing that he published the vulnerability. With commit messages like “Update ActivityPub helpers, improve private account handling” it is a dead giveaway when one knows what they’re looking for.
So I reached out to Dan:
[…] Unfortunately, now that the commits are pushed in a public repository the tooth paste is out of the tube. It’s not possible to announce a grace period for the instance operators to prepare for a coordinated update. Therefore the way to go would be to publish a release and announce that admins should update their servers asap.
The last release v0.12.4 is almost half a year old, so a release would contain a lot of changes, which could further delay admins updating their servers.. Is it possible to go out with a v0.12.5 first (or in parallel) that contains only the back-ported security patches?
His only reaction was:
Yes, I am preparing the release as we speak!
A few days later the release dropped. While the version increment (v0.12.4 to v0.12.5) implies a minor update, it’s a huge leap. We’re totalling more than 450 commits, including the requirement of a new version of PHP, which on some distros requires a major system upgrade. In the release notes it’s not obvious in any way that there is a critical vulnerability fixed in this release, while they mention an important privacy fix in a Mastodon post.
Disclosure
Ideally, serious security issues in open source software are handled like this:
- Problem is reported to maintainers
- Fix is developed in private
- Public announcement of a security release on a specified day
- Admins and package maintainers prepare for the update
- Fix is published
- Admins patch their servers immediately
Other projects, like Mastodon, have done it like this in the past.2
That’s what I intended and asked for. Instead my requests were ignored and communication was poor.
Conclusion
I’m disappointed by how Pixelfed managed the vulnerability. From a project with (supposedly) more than 150k monthly active users3 and generous funding4 5 6 7 I expect better.
This is not the first incident of Pixelfed handling security matters poorly. A similar situation unfolded a few months ago, when a bug8 left hundreds of instances vulnerable which apparently resulted in stolen S3 API keys.9 At time of writing, there is also a three years old GitHub issue that reports 2FA being broken. 🤯
The maintainer has a track record of hostile behaviour towards the community. His new project Loops is making headlines as “the fediverse answer to TikTok”, despite (still?) being closed source, not federating and having questionable terms of services.10
Crucially, the fediverse is based on trust. I think there is a discussion to be had about the impact that reckless behaviour has on the general environment of the fediverse. Pixelfed is one of the most popular ActivityPub implementations, and that comes with a certain responsibility. Furthermore we have to work on better approaches to handle trust. For example by providing more features to limit what is shared with whom, and transparency ingrained into the user interface. We need tooling that allows instance admins to stop federation with known vulnerable software versions. Both to mitigate risk and force instance admins to upgrade.
Timeline
Times are in UTC
- 2025-03-14: Vulnerability is discovered and analysed
- 2025-03-16 15:00: I create the GitHub Report and send an email to hello@pixelfed.org
- 2025-03-17 21:30: First reaction by Dansup
- 2025-03-19 08:30: Dansup publicly pushes fixes
- 2025-03-21 07:50: I ask Dansup for a backport security release
- 2025-03-24 06:00: Dansup releases pixelfed v1.12.5
- 2025-03-25 17:00: Release of this post
Footnotes
-
Even blocking that user does not help because, surprise, Pixelfed seemingly doesn’t have federated Blocking implemented. You could remove all your pixelfed followsers, or even defederate their instance.. That would prevent leaking new posts - but the damage is done for the existing posts and they stay on the other server. ↩
-
https://www.kickstarter.com/projects/pixelfed/pixelfed-foundation-2024-real-ethical-social-networks ↩
-
https://github.com/pixelfed/pixelfed/security/advisories/GHSA-gccq-h3xj-jgvf ↩
-
One instance admin told me about this happening to them. ↩