We found and fixed a rare race condition in our session handling
github.blog
github.blog
It is funny how they make it seem like they are super cautious. When in fact they had no other choice than do it when you rephrase the problem as " there were maybe some people who got the admin right of the github account of some of our customers"
Default to last postcode but display it on the confirmation screen "Sending your Pizza to LE13 XYZ" - amazon does it right (as you'd mostly expect)
Login limits are a good thing. It means that the chances of someone taking over your account and using it long term is minimized.
Logs you out of all services. It‘s a nightmare as a user, but I don‘t want to stick those tracking to cookies around longer than a single day.
Anything you intentionally want to retain can be added to the exceptions list.
Perhaps I should just accept the consent and remove cookies regularly, instead of being careful with the consent forms... Hmm, good idea.
They are lucky this wasn't much, much worse.
Why does the error reporting service need to call the web thread back anyway?
Yet another case where immutable-everything would have prevented this hairball from accreting in the first place.
An IDOR is embarrassing - this was a complex bug and Github's investigation/response was great here. I'd be proud if my company handled a security incident like this.
The follow-up report is good, but it doesn't matter when the conclusion is "our footguns backfired".
Probably it wasn't that obvious in the code to see.
But if they would've used immutable objects, or at least pure functions with scalar parameters in the first place, such an error would never occur.
And Unicorn is pretty old. It wouldn't surprise me if this object reuse dates back to mongrel's code base, back when doing anything with threads in Ruby would have been unusual.
It's still bad code on unicorn's part (when I got to that part of the post I said "Unicorn does WHAT?" out loud), and GH should have been more careful introducing threads to an environment that didn't have concurrency before, but every step of the chain is pretty understandable IMHO.
Sobering thought for me is how probable it is that a similar issue might be present in other platforms. The title calls it "rare" but given (a) the number of complex-enough web/app platforms in use today, (b) just the endless number of ways/infra permutations for "doing x", and (c) the size of a typical company's codebase and dependencies, could it be a more common occurrence than we think?
Total anecdote, up for you to believe: Back in April 2016, one of my then-housemates bought a brand-new Macbook Air. A week into his ownership, he exclaims surprise that he is logged-in to another Facebook account, someone we are totally not acquainted with. I borrow his MBA and try to investigate but, alas, I lack expertise for it. The only relevant thing I can eke out is that they both went to the same event a week or so ago and probably shared a Wifi AP, where a "leak" could've happened. Or maybe a hash collision in FB's side?
I would've reported it to FB but then I don't have enough details; with the two conjectures I have, I'm basically holding water with a sieve. But now the thought that someone I totally don't know might end up logged-in in my FB account is a possibility that bothers me. And I have no idea how to prevent it other than minimizing what could be compromised (and no, "Get off FB!" just isn't in the cards, I'm sorry).
Kudos for Github for investigating and fixing this issue. Another thought that occurred to me while writing this is how many users of other platforms might be filing issues like this but can't provide enough detail and so their reports are eventually marked as "Could Not Reproduce". Not exactly leaky sessions but just general security mishaps that they would struggle to describe to support.
> Hello everyone! This is forum regular fronzelneekburm, posting from the account of some poor Chinese guy that gog randomly logged me into. I'll explain the situation in further detail in another post from my actual account once I logged out of this one.
That's a weird one, thanks for sharing it!
> The underlying bug existed on GitHub.com for a cumulative period of less than two weeks at various times between February 8, 2021 and March 5, 2021. Once the root cause was identified and a fix developed, we immediately patched GitHub.com on March 5. A second patch was deployed on March 8 to implement additional measures to further harden our application from this type of bug. There is no indication that other GitHub.com properties or products were affected by this issue, including GitHub Enterprise Server. We believe that this session misrouting occurred in fewer than 0.001% of authenticated sessions on GitHub.com.
If we use the 0.001% figure and assume all 56 million of their users are authenticated, then this occurred with 560 sessions at most.
You are right in that when something only happens to a minuscule % of users is hard to investigate and repro. But at the same time something as critical as "I logged in and was authenticated as another user" should probably be immediately escalated to the highest levels within your company.
1: https://github.blog/2021-03-08-github-security-update-a-bug-...
I believe they are referring to the rarity of hitting this bug during the period of time it was broken, and not the rarity of the bug in general.
People were speculating on this here: https://news.ycombinator.com/item?id=26395138, and I even left a comment about this type of bug, but on Node. This category of bug, where session data leaks across requests, is actually incredibly common.
My most fun theories are
1. Somewhere in the supply chain, someone logged in to their FB account on this machine.
2. During the first week he owned it, someone logged in to their FB account on this machine.
3. You ex housemate lied about this. Crazy people are rare, but MUCH more common likely bugs that could cause this.
Given two users in extremely close geographic proximity, the chances of them having manually logged in on the device themselves with your housemate either forgetting or not knowing are much much higher than the chances of an authentication exploit in the world's most popular site that would have gone undiscovered for a subsequent five years at this point.
Probably somewhere between the two (but closer to the "they just logged in on that computer" side of things) is the chance of the LAN being some eldritch abomination that just occasionally serves responses to the wrong local IPs, but that's not something any online service needs to care about.
This could have worked in ye olde pre HTTP days, but with HTTPS this cannot reasonably have happened.
We recently fixed a bug with incomplete requests by noticing in nginx logs that the size of the headers + the size of the body was the size of the buffer of a linux socket.
No, not affiliated with datadog, and not convinced the cost/benefit ratio is positive for us in the long run.
It's not uncommon to see companies talk about processing terabytes or even petabytes of log data per day. In the end the storage isn't that expensive [1], and you can discard everything after a little while.
[1] Assuming of course that any company generating a petabyte of logs every day is doing so because they have an enormous number of customers, AFAIK the cost of storing 1PB of logs on something like Glacier is in the 100-300k USD/yr range. Absolutely a tolerable expense for the peace of mind you get by knowing you can go back in 2 months and find every single request that touched a security vulnerability or hunt down the path an attacker took to breach your infrastructure.
What we know based on this report:
- This bug was live in production for at least 3 days.
- It was common enough to happen numerous times in the wild.
- GitHub says that they identified some specific user sessions that may have been impacted by this bug by analyzing their logs.
What we do not know:
- How many leaked user sessions were identified in the followup investigation?
- Is GitHub confident that they identified all leaked user sessions?
- Was there any evidence of malicious use of these sessions?
- Did GitHub notify the users whose sessions were leaked?
- How long/for how many requests could these sessions have been valid between their leak and when they were definitively revoked on the 8th?
- Is it possible that any private user data was leaked? If so, what data?
Things that are still unclear:
- GitHub provides an estimate of the prevalence at 0.001%, but doesn’t state how many leaked sessions were actually identified.
- Is GitHub confident that they identified all leaked user sessions? This has bearing on my question about notification of users whose sessions were leaked; if they did not identify all leaked sessions, they obviously could not contact those users.
- GitHub states that this bug could not be intentionally triggered by a malicious party (why?), but they don’t state whether any of the natural occurrences may have been used maliciously.
- How long/for how many requests could these sessions have been valid between their leak and when they were definitively revoked on the 8th?
- Is it possible that any private user data was leaked? If so, what data?
People with technical understanding can infer most of this information, but I think GitHub should issue some straightforward answers rather than beating around the bush.
(Correction to my original comment: the bug was live for a cumulative period of “less than 2 weeks”)
Because this bug requires two users making authenticated requests at nearly the same time. An attacker cant force their target to perform actions, especially not in the same small time frame that is likely required (milliseconds? less?). I suppose if the malicious party didnt care what account they got swapped into, and github didnt rate limit their requests, perhaps they could trigger this bug with enough time?
Given that they revoked all current logins, rather than just the affected ones, I assume that they have no way to tell which sessions were affected.
"While our log analysis, conducted from March 5 through March 8, confirmed that this was a rare issue, it could not rule out the possibility that a session had been incorrectly returned but then never used."
Those sessions are now invalidated and apparently never used
Impressed with GH folks in finding this, though I wonder how many more of these types of bugs are out there in Ruby libs. It'd be nice to make this more difficult to do, a la FP languages, by making it really intentional to modify state. Maybe in Ruby 4??
Was Unicorn itself not thread safe? It sounds like Unicorn had the requirement that once you send back the request to the user you must stop accessing the request object. Maybe Unicorn didn't document that requirement clearly, but even if it's not documented it's not clear to me that Unicorn itself is not thread safe, but rather that it has undocumented pitfalls that can easily lead to thread unsafety.
I believe that this could be addressed with an architecture that favored single-threaded processes that can handle a single request at a time. Said processes would be numerous, have a (best-effort) low memory footprint, and be pooled under some master process written in a concurrent/performant manner.
The result wouldn't be necessarily pretty or resource-efficient (especially in terms of RAM), but it also can be seen as a consequence of Ruby's requirements for safety.
An org like GH almost certainly can afford using more/beefier servers it it means that entire classes of bugs will go away.
To be fair to Ruby, most languages are largely mutable and without any strong thread-safety guarantees. Does that make it okay? No, but it’s far from a Ruby-specific problem.
> I believe that this could be addressed with an architecture that favored single-threaded processes that can handle a single request at a time.
Unicorn (the application web server GitHub is using) does follow this model. GitHub added their own additional thread within that process that broke that design in an unforeseen way.
This doesn't mean they are only doing good™ with GitHub, but destroying it and open source vendors would be counted to their interests.
Except the ones that are just the universe conspiring against you, like bit flipping in the SM-1800 because of heavily irradiated cattle being transported on a nearby train line.
But there's a thought in the back of my head wondering what kind of stuff hit the fan, and wondering who had to create the presentation. Was it the person who checked in the broken code, was it the bug fixer or his manager, or was it a pm that got stuck having to write up what happened?
I guess the only way we will know that is if there’s ever a vulnerability in the session handling code of the mail server that they use also xD
ANY time ruby 1.8 raised a timeout, it was silently swallowed, and Unicorn returned the adjacent request session_id. We had to constantly defend against any code that might hit the native timeout.
So they already conceded that their code is fundamentally not thread-safe and they decided to let it slide.
It's never a good idea to perform multithreaded operations within the web server, unless you have a strong knowledge of its internal workings. For instance, if you miss manage threads in an IIS web application you can end up having your web requests queued since the web server has by default a fixed number of worker threads to serve web requests.
From their bug description it's clear that ruby on rails is no exception to this rule. In such cases where background processing is required due to some condition triggered by a web request it's best to stick with a propper queue system [1]
[1] https://thomasvilhena.com/2019/07/using-queues-to-offload-we...
Immutable data forces you to design your system in a different way and remove layers of complexity.
But not what I meant. I meant that it was a conscious decision to use an object pool and re-use objects.
That doesn't happen with immutable by default languages.
Ergo, these particular kinds of bugs just don't happen.
This type of thing is why PHP's shared-nothing model is vastly undervalued IMO.
Does PHP have the ability to use threads if you really need it? Yes.
Are they generally a PITA to use? Yes.
Is this a problem for the vast, vast, vast majority of projects? Not in the slightest.
Having a separate invocation of the runtime for each request is a feature, not a failing.
https://github.com/puma/puma/blob/0cc3f7d71d1550dfa8f545ece8...
and Client objects are not reused:
https://github.com/puma/puma/blob/0cc3f7d71d1550dfa8f545ece8...
But since it's a multi-threaded server, it will exercise arbitrary code (Rails core, your app, depended-on gems) in a concurrent manner.
This can unmask race conditions that wouldn't manifest themselves under a single-threaded environment.
...that's been my experience with Puma - brilliant server in itself, somewhat risky approach when considering the Ruby ecosystem.
But I don't see the old behavior described as "allocates one single Ruby Hash that is then cleared (using Hash#clear) between each request"...even after poking around in http_request.rb and other places. I reads like the pre-patched version would just re-use the hash as-is, only overwriting/adding if it saw a new key or value, but not deleting any keys.
[1] https://yhbt.net/unicorn-public/66A68DD8-83EF-4C7A-80E8-3F1F...
Admittedly, that's an older copy, I'd have to look up a newer copy of the source to see if that function or a similar one still exists.
The change was made in Unicorn v6 according to https://yhbt.net/unicorn/NEWS.html so I'd have to find code earlier than that, maybe.
https://yhbt.net/unicorn.git/tree/ext/unicorn_http/unicorn_h... looks like it's still there.
If I'm reading this right, the init function in the above unicorn_http.rl function is what's referred to as HttpRequest#new thanks to https://yhbt.net/unicorn.git/tree/lib/unicorn/http_request.r... but I might be misreading this.
Actually... compared to uWSGI's source code, this is quite pleasant to read, warts and all. :D
Who in the heck thought this would be a good idea? For the record, back when Desk.com existed, I also saw strange session issues that were probably complex and irreproducible race conditions as well.
Note that in Elixir/Phoenix, this class of bug is literally impossible, thanks to immutability-all-the-way-down. Turing bless it.
To prevent these kind of problems we have a few approaches, but the main way is to prevent shared mutable state. To do this we have a custom C# code analyzer (source here: https://github.com/Brightspace/D2L.CodeStyle/tree/master/src... , but it's not documented for external consumption at this point... ImmutableDefinitionChecker is the main place to look.) It goes like this:
[Immutable]
public class Foo : Bar {
// object is a scary field type, but "new object()"
// is always an immutable value.
private readonly object m_lock = new object();
// we'll check that ISomething is [Immutable] here
private readonly ISomething m_something;
// Fine because this lambdas in initializers can't
// close over mutable state that we wouldn't
// otherwise catch.
private readonly Action m_abc = () => {};
// see the constructor
private readonly Func<int, int> m_xyz;
// danger: not readonly
private int m_zzz;
// danger: arrays are always mutable
private readonly int[] m_array1 new[]{ 1, 2, 3 };
// ok
private readonly ImmutableArray<int> m_array2
= ImmutableArray.Create( 1, 2, 3 );
public Foo( ISomething something ) {
m_something = something;
// ok: static lambdas can't close over mutable state
m_xyz = static _ => 3;
// danger: lambdas can in general close over mutable
// state.
int i = 0;
m_xyz = x => { i += x; return i; };
}
}
Here we see that a type has the [Immutable] attribute on this class, so we will check that all the members are readonly and also contain immutable values (via looking at all assignments, which is easy for readonly fields/properties). Additionally, we will check that instances of Bar (our base class) are known to be immutable.Any class that were to extend Foo (as a base class) will be required to be [Immutable] as well. There's a decent number of aspects to this analysis (e.g. a generic type can be "conditionally immutable" -- ImmutableArray<int> is, but ImmutableArray<object> is not), check the source if you're interested.
We require all static (global) variables be immutable, and any "singleton" type (of which we have tens of thousands if I remember correctly) also must be.
Another important thing we do to is to cut off any deferred boot-up code from accessing customer data (via a thread-local variable that is checked before access through the data layer). This prevents accidentally caching something for a particular customer inside, say, a persistent Lazy<T> (or the constructor for a singleton, etc.)
We've adapted our code base to be very strict about this over the last few years and it discovered many obscure thread-safety bugs and doubtlessly prevented many from happening since.
Also another reason not to mix the main application code with the authentication/authorization code. Setting security cookies should be done by dedicated code in a dedicated process to avoid this kind of interaction, and the load-balancer layer should filter the setting of security cookies from non-security backends to enforce it.
There are of course safe ways to do all of this kind of stuff, but there are _so many_ unsafe ways to do things, and accidentally share things across requests. At least in the Django world I think ASGI is going to make it more likely that stuff is done the right way but there's a big learning curve there.
Shared memory environments are very unsafe and we should be working towards a memory model where data that belongs to different organizations can never get mixed. This isn't impossible, but it requires new programming languages that allow us to express which memory areas can't get mixed up.
We've got to start designing our backend infrastructure differently such that this entire class of memory errors can't ever occur.
In other cases, you might assume containers would be enough separation, or maybe different databases (sharding, for example). One approach I've considered but not yet implemented is to take end-user JWT cookies and use them to authenticate to a database such that a user can only ever see the rows they should be allowed access to, and all other SQL queries would fail.
But no matter how you implement a security control, it's possible to make mistakes and allow others to see data you didn't intend to share with them. Even Rust, famous for its explicit memory ownership model, could still have similar bugs, they just might be a bit more obvious to spot in some session connection pool implemented incorrectly, for example, or maybe exposed via a compiler optimization gone wrong. Code review will still likely be one of the primary defenses of this kind of bug, though hopefully better tooling and language support to detect race conditions or shared ownership warnings will help over time?
Yes, performance is an issue with that old approach. Around the turn of the century I was responsible for a Perl web application that ran as a CGI script, and to speed it up I wrote my own Perl httpd server that had my web application code pre-loaded. Then for each request I forked a new process, which carried over all of the pre-loaded code and already-running perl.exe, so there was very little startup time (compared to a normal CGI script startup.) I was careful to take full advantage of the operating system's Copy-on-write memory semantics, so that the only memory that had to be allocated for the new process was Perl's runtime stack. All of the memory containing the application code was shared across processes because it never got written to.
This web application is still running today, though I left the company in 2010. Back then, it was handling about six million requests per day, about 75% of which was during US daytime working hours. So, around 160 requests/second, spread across five or six servers that were getting old in 2010.
The modern frameworks have taken the same concept a step further, and replaced the process fork with a thread pool. That's faster, but it allows memory leaking in a way process forking does not. You have to go out of your way to share memory between processes, but with threads it's easy to do accidentally.
Ultimately, I don't think one can ever be happy about "there was a giant security hole in our app, and we only found out because a customer noticed it and reported it to us, it turns out it was in a random library that we didn't even write and so now that we found this everything is fine!" Everything is not fine, and this will just happen again.
It saved me more than once in similar situations with several threads.
Except when those events are associated with private user data or behavior.
It can simplify certain abstractions of course so that you can provide easier tools for devs to avoid footguns.
Even if that particular bug is actually harder to stumble into, there's no guarantee the space of thread synchronization issues one might stumble into is inherently smaller for Rust than for Ruby, so all you're doing is exchanging one class of failure modes for another.
In most other languages, sharing mutable data across threads is unprotected. You are lucky if you get a compiler warning. Rust disallows that completely. You cannot share mutable data across threads without first wrapping that data in thread safety constructs.
That's what happened here. A mutable variable was shared between threads and updated from different threads. Rust makes it obvious that this variable is accessed from multiple threads even if it doesn't prevent 2 threads from writing to the variable at wrong times. That's where it is a partial solution to the problem.
Sharing is as easy as wrapping the thing in a Mutex, and if you are a programmer that sees something wrapped in a mutex you should immediately start thinking of the thread implications. You don't have to, but you should.
That mutex follows the variable around anywhere you want to start changing it.
In a language like C or Ruby, that's not something that is patently obvious. Shared variables aren't wrapped with anything. It's perfectly understandable for someone to not realize this bit of shared state is mutated across threads.
I'm not judging the choices of technology, but I'm thinking it was easier for them to buy Github outright then try to compete with it. I assume Github still operates independently, with engineers who are familiar with this stack.
also, what does familiar to them means in the MS context? i’m sure the people that work on github are familiar with the stack
I wasn't sure how independent Github is from Microsoft after the acquisition. My thoughts were initially in moving engineers around internally from within Microsoft to work on Github.
Multi-threaded race conditions in a Rails app under Unicorn Rack takes a lot of experience in specific tooling.
> We initially thought this to be an internal reporting problem only and that we would see some data logged for an otherwise unrelated request from the background thread. Though inconsistent, we considered this safe since each request has its own request data and Rails creates a new controller object instance for each request.
I mean this is like an SNL skit: "though inconsistent, we considered this safe" -- inconsistency, no matter where it's happening, literally means your code isn't safe. Like, jeez... this is GITHUB we're talking about. I mean, I give money to GitHub -- this is pretty serious and we should really stop with the "omg I love these detailed post-mortems!" stuff. A billion-dollar company knowingly having code that's not thread safe in their flagship product and going "eh, it's probably fine" is bonkers to me.
No software maintained by a large team will be bug-free forever, and that includes security bugs. The response to bugs is what matters. In this case Github's response was quite mature; in Gab's case, it wasn't.
This is irrelevant.
There is no excuse for this, especially if you are a billion dollar company.
Github Actions has had many problems that affect many people this year alone. While the missed deadlines and stress for people can't be quantified by Github, they are happening.
I'd urge Github to take ongoing service interruptions in Github Actions more seriously. I hope Github will take a stronger stance on providing customer communication on migration timelines between Packages and Container Registry.
Just because a bug or service interruption doesn't risk random user session exposure doesn't mean it isn't important to communicate about and describe how the business is working to prevent it from happening again.
I find it ironic that the largest repository of open source code is closed source and and owned by the largest closed source software company on the planet, yet they somehow still "believe in transparency."
That's like McDonald's "transparently" recalling burgers because of a chemical contamination, and then when you ask what else is in the burgers they tell you it's a secret.
Just one example: what if someone pasted an error log message into a commit message that included a piece of PII?