pledge(2)-ing and unveil(2)-ing the Drogon web application framework

I'm drunk on a Friday night again. This time I decide to investigate how to use OpenBSD's pledge(2) and unveil(2) to enhance the security of the Drogon web application framework. Drogon in written in the C++ programming language, not known to be safe. But in truth, as a maintainer of Drogon, non of our CVEs are related to memory at all. Most either they are are directory traversal and/or abusing the HTTP spec. Anyway, in case we messed up, I still want to make sure that Drogon is as secure as possible. And OpenBSD's pledge is a really good way to do that.

About my drink, I added too much gin into my poor-man's Long Island Iced Tea. It's bitter and strong. Had to add more coke to make it drinkable. Should just got some flavored vodka instead.

pledge, is a C fuction on OpenBSD (and SerenityOS) that allows a program to restrict its access to the system. For example, a program can pledge to only use stdio and network. If the program tries to set the system time, it's considered a violation and killed immidiately. Think it like SELinux, but many default policies are already written for you. For example, the following C.

int main() {
    pledge("stdio inet", NULL);
    // From this point on, the program can only use stdio and network.
    // It cannot, for example, fork or exec.

    char buf[1024];
    scanf("%s", buf); // oops vulnerable to buffer overflow, doesn't matter
                      // the attacker can't fork or exec to run commands anyway

    // This will cause OpenBSD to abort the program.
    // fork();
    return 0;

Unveil, on the otherhand, limits a program's access to the filesystem. It's A LOT harder to create a reverse shell if you can't execute stuff in /bin and /usr/bin.

int main() {
    char* cwd = getcwd(NULL, 0);
    unveil(cwd, "r"); // Only allow read access to the current directory
    unveil(NULL, NULL); // This is required to lock down the filesystem
                        // otherwise the program can't do anything
    system("ls"); // This will fail because the program can't access /bin/ls
}

Pledging and unveiling Drogon can be a bit complicated. The framework does quite a few things that is hidden from the user, that could be supprising.

  • Memory mapped files to handle large request bodies and decompressing
  • DNS reqires file read if using c-ares
  • Makes a cache directory for hashed buffers upon startup
  • Calls stat on documents to check when they were last modified

The simplest way to pledge and unveil Drogon is to do it before the framework is initialized. The following code is a good start. Add more path/permissions as needed.

using namespace drogon;
unveil(app().getDocumentRoot().c_str(), "r");
unveil(app().getUploadPath().c_str(), "rwc");
// Remember to add `unveil` to the end of the list
// if you add more unveil calls later.
pledge("stdio rpath wpath cpath inet", NULL);

But, we can do better. Drogon only needs to create the cache directory at startup. And the queueInLoop function on the main thread is only executed when the server is done initializing. If you know you no endpoints supports uploadingand have set client_max_body_size == client_max_memory_body_size. The following is stricter and should provide better security. In fact, the following is what I'd run for this blog if it were running on OpenBSD. (It's not, but I'm emulating unveil with landlock on Linux, can't emulate pledge yet, you need a few more paths on Linux too).

using namespace drogon;

// running after finished initializing
app().getLoop()->queueInLoop([]() {
    unveil(app().getDocumentRoot().c_str(), "r");
    unveil(app().getUploadPath().c_str(), "rw"); // "c" is not needed
    unveil(nullptr, nullptr);
    pledge("stdio rpath wpath inet", NULL); // "cpath" is not needed
    // The "create" class is dropped since we don't need to create files.
});

It's possible to further improve the security of Drogon by unveiling the configuration file (if needed), ruling out any chance of vulnerabilities in the JSON/YAML parser. But I think that's a bit overkill.

What does it stop?

  • Arbitrary directory traversal
  • Reverse shell
  • Most forms of buffer overflow + ROP attacks
  • Most forms of ret2libc attacks
  • Overwriting configuration files
  • Arbitrary file upload
  • Access to /tmp

What does it NOT?

  • VERY advanced ROP directly calling libc
  • Request smuggling
  • IDOR, CSRF, XSS, SQLi, etc.. (duh, unrelated to OS security)

Limitations

Pledging and unveiling is not a silver bullet. They are very easy to deploy security measure that can be used to limit the damage of a vulnerability, stop some out right some times and makes the life of attackers painful. pledge does not and cannot distinguish between LAN and WAN addresses. If an attacker can figure out how to defeat ASLR, they can still ROP their way to opening a socket and accessing the DB that way. However, I had to assume it's virtually impossible to write a Postgres client by ROPing.

Author's profile. Photo taken in VRChat by my friend Tast+
Martin Chang
Systems software, HPC, GPGPU and AI. I mostly write stupid C++ code. Sometimes does AI research. Chronic VRChat addict

I run TLGS, a major search engine on Gemini. Used by Buran by default.


  • marty1885 \at protonmail.com
  • Matrix: @clehaxze:matrix.clehaxze.tw
  • Jami: a72b62ac04a958ca57739247aa1ed4fe0d11d2df