Thoughts on software security
Created at 2024-04-15
Modern software is a house of cards. The Linux kernel itself contains over 100,000 lines of code, drivers amount to approximately 7 million lines of code. The node.js package ecosystem is known for its hard-to-control package supply chain. There is so much code, and so many authors, we trust blindly every day. Much attack surface is even unnecessary, such as an obscure REST endpoint of your probably unpatched Nextcloud or Wordpress instance. If I think about it, working with my computer is like hanging on a chain, the links of which are handmade by thousands different people.
Many authors are very cautious, but still, security vulnerabilities happen (should I mention recent incidents?). In the past, our security-critical software had dangerous vulnerabilities that were only found months later. If nothing has changed, the software we use *right now* has dangerous vulnerabilities we don't know about, too, and resourceful actors will occasionally exploit some of them before the white hats find them. Indeed, according to the zero-day tracking project, every year since 2006, there have been over 20 zero-day vulnarabilities, i.e. vulnerabilities actively exploited before a patch was available. Of course, not *all* of them are equally dangerous, but some are. Moreover, these are only the *known* zero-days, the ones we noticed; we can't be sure these are all.
Hence, I find it really hard to feel confident in the security of our software systems.
So what should we do about it beyond regularly patching our software? I don't know, as I'm not a security engineer. In this article, I just want to present a few concepts I personally find relevant for my treatment of software security, in particular regarding hosting software on a server and managing the security of my online accounts.
With great privilege comes great responsibility
When we hear of privilege escalation vulnerabilities, we think of kernel bugs that allow an unprivileged process to gain root privileges, but the problem is much more general.
For example, let us assume we write a script `tool.sh` (owned by the current user ), make it executable and later run it with `sudo`. We have created an opportunity for privilege escalation: A malicious actor that managed to execute code as can easily append a malicious command to `tool.sh`, and the next time we run `sudo tool.sh`, the command is executed with elevated privileges!
I propose to summarize this situation in the following commandment: *When a user is allowed to modify an executable, users with higher privileges should not execute that file.*
As a more general rule: *The more rights the unprivileged users have, the more cautious the privileged ones need to be.*
The Bell-LaPadula model is concerned with the flow of information and is another example of this rule. It is designed to avoid the leaking of confidential information. The slogan is "write up, read down": Users and resources are assigned confidentiality levels. Information must not flow down: A user can read all the resources below themselves, as this constitutes an upward flow of information. Therefore, the more privileged a user is, the more they can read; this is the idea of a security clearance. What's more surprising is that in the Bell-LaPadula model, users are not allowed to write resources below them, as this would constitute a downward flow of information. Hence, the more privileged a user is, the *less* they can write! This is quite a draconian measure, as a careful user might make sure not to leak their confidential knowledge, and even secret service officials violate it when they give interviews to the press. (One might say, humanity has gained fire through a Protean security breach of the Gods' Bell-LaPadula multi-level security policy, where information may only flow upwards the Olympus. This could also explain why the Gods rarely present themselves to humans.)
Granting more permissions to less privileged users may improve security
Effective security is not about taking privileges away from non-root users, it is about *having a distribution of privileges that allows each user to do their job with as few rights as possible*. Otherwise, the less privileged users will be of no use and everyone will use `sudo` all the time. I think we need to think a lot more about fine-grained permissions, for example:
- Imagine hosting a web service. It writes into a log file and therefore has write access to it. Now imagine an attacker achieving remote code execution in the service. They have the same permissions as the service and will be able to cover up their traces by deleting suspicious log entries. The problem is that we coupled addition and deletion of lines to the same privilege: Either the service is allowed to log, in which case it is also allowed to delete lines, or it is not allowed to delete lines, in which case the log file will stay empty forever, as the service cannot add lines either. The dilemma can be solved by giving the service append-only access to the log.
- Many people have a single e-mail address, which they use for account registrations as well as everyday communication. They often access their mail from their mobile phones because it allows them to answer more quickly to important messages. However, this requires them to bring the permission to access their e-mail to insecure places: Their phone, which they take everywhere and could easily lose, needs to store their password. When somebody gets access to this password, they will not only see all the less critical everyday messages; they will be able to read password-reset e-mails, gaining total control over the online accounts of the victim. From a security standpoint, it would be better to have a second e-mail address, for everyday communication. This way, there is no necessity to carry the key to the whole online identity around.
- I have a Posteo e-mail address, and with it comes a calendar service. While I am very satisfied with their service, I would really wish that they allowed me to use a separate set of credentials for the calendar. While e-mail is highly sensible, I need to carry my calendar everywhere around.
Create extremely simple protocols and software
I have a self-hosted Nextcloud instance, but I only use it to manage and synchronize my calendar. Once I decided to get rid of it, implementing my own minimalistic CalDAV server. Naïvely, I imagined I'd write a simple backend that
- can read store iCal files in a database,
- exposes endpoints for the retrieval of all calendars and all entries in a calendar to allow synchronization (I'm using the DAVx5 app).
Reading the RFCs, here is what I learned:
- CalDAV is an extension of WebDAV.
- WebDAV is based on HTTP (fine) and XML (oof).
- CalDAV relies on an XML-based query format to retrieve various things (principals, calendars, calendar items, ...), and there are comprehensive filtering functionalities ("give me all items in the following time span" etc.).
- A valid CalDAV implementation must provide most of these functionalities.
I finally ditched the project because it was too much of a time investment for a small hobby project to even implement the bare minimum. The complex query language feels over-engineered for my use case (synchronization).
I really wish our protocols were dead-simple:
- Requests and responses are very easy to parse.
- There are few surprising special cases.
- There is one, and only one, way to express a request.
- Implementation of the protocol should not be harder than writing a basic LISP interpreter; in other words, it should be a task that can be easily finished in a day, given a good specification.
- It should be possible to reduce the implementation effort by stripping unnecessary features.
- The protocol should subject to as few changes as possible, so that implementations can be hardened once and used forever.
(Interestingly, XML was designed to be simple, and maybe it is really simpler than older formats. Or it is as simple as it gets if you want to achieve the high extensibility of XML. Either way, XML is complex enough to offer lots of security pitfalls)).
Why are our de-facto-protocols so complicated? For lower-level protocols, think HTTP, the reason is most probably performance: Many complications, such as "Connection: keep-alive" and caching, exist for precisely this reason.
From theory to practice
I recently thought a lot about security, for one, because I am in a process of moving my web services to a Virtual Private Server, which means more responsibility for me. In the process, I thought about security risks. I decided to use a static site generator for this blog instead of the Wordpress page I once had, but I have not yet gotten rid of Nextcloud (because of the calendar), which is also a one-size-fits-all piece of software you need to carefully configure and patch. So for now it's running behind HTTP basic authentication, in which I trust more than Nextcloud due to its inherent simplicity.
SELinux allowed me to implement some of the privilege-related ideas. I like that it is so fine-grained, and I find this a much more convincing benefit than the fact that it realizes Mandatory Access Control.
Either way, security is a process, not a product, so there's still room for improvement. Only hosting the things I'm confident of feels a bit ascetic, but that's where I am right now.