Hardening Dillo with unveil and pledge
Hi list, I have been experimenting with unveil[1] and pledge[2] on Dillo to reduce possible attack surface. Right now these system calls are exclusive to OpenBSD, but there are projects[3] working to bring them to Linux and other systems. Basically, unveil restricts which areas of the filesystem a program can access, and pledge restricts which system calls the program is allowed to make. [1] https://man.openbsd.org/unveil.2 [2] https://man.openbsd.org/pledge.2 [3] https://justine.lol/pledge/ Initial testing indicates that both of these features are working correctly with Dillo. I am including a patch which provides some basic filesystem protection, and limits Dillo to the minimum amount of syscalls possible. If anyone has questions or comments, they are welcome. Regards, Alex --- a/src/dillo.cc Fri Jun 14 22:27:18 2024 +++ b/src/dillo.cc Sat Jul 20 15:16:42 2024 @@ -24,6 +24,7 @@ #include <stdio.h> #include <unistd.h> +#include <err.h> #include <stdlib.h> #include <time.h> #include <sys/types.h> @@ -397,6 +398,24 @@ int main(int argc, char **argv) FILE *fp; srand((uint_t)(time(0) ^ getpid())); + + // Unveil and Pledge + if (unveil("/usr", "rx") == -1) { + err(1, "unveil failed"); + } + if (unveil("/tmp", "rwc") == -1) { + err(1, "unveil failed"); + } + if (unveil("/etc", "r") == -1) { + err(1, "unveil failed"); + } + if (unveil("/home", "rwc") == -1) { + err(1, "unveil failed"); + } + if (pledge("stdio rpath wpath cpath inet unix dns tty proc prot_exec", + NULL) == -1) { + err(1, "pledge failed"); + } // Some OSes exit dillo without this (not GNU/Linux). signal(SIGPIPE, SIG_IGN);
Hi Alex, On Sat, Jul 20, 2024 at 04:02:54PM +0200, a1ex@dismail.de wrote:
Hi list,
I have been experimenting with unveil[1] and pledge[2] on Dillo to reduce possible attack surface. Right now these system calls are exclusive to OpenBSD, but there are projects[3] working to bring them to Linux and other systems.
Basically, unveil restricts which areas of the filesystem a program can access, and pledge restricts which system calls the program is allowed to make.
[1] https://man.openbsd.org/unveil.2 [2] https://man.openbsd.org/pledge.2 [3] https://justine.lol/pledge/
Initial testing indicates that both of these features are working correctly with Dillo. I am including a patch which provides some basic filesystem protection, and limits Dillo to the minimum amount of syscalls possible. If anyone has questions or comments, they are welcome.
Thanks for the patch, I had just opened an issue on this topic a few days ago: https://github.com/dillo-browser/dillo/issues/225 I believe it would be nice to move the network facing code away from the parsers, so the parser code cannot use the network or read the file system. This requires separating them in different processes. I also have to take a look at Justine implementation of pledge(2) for Linux. Constraining the current design may already lead to some improvement.
+ if (unveil("/home", "rwc") == -1) {
We may want to constraint this a bit further, so a malicious actor cannot read anything from /home/.config. Maybe only /home/.dillo and the downloads directory would be suitable?
+ if (pledge("stdio rpath wpath cpath inet unix dns tty proc prot_exec",
Does this work with plugins, when the dpid daemon is not running?, as I believe it has to fork and exec the dpid program. Easy test: $ dpidc stop $ dillo dpi:/bm/ Best, Rodrigo.
Hi Rodrigo, On Sat, 20 Jul 2024 16:34:47 +0200 Rodrigo Arias <rodarima@gmail.com> wrote:
+ if (unveil("/home", "rwc") == -1) {
We may want to constraint this a bit further, so a malicious actor cannot read anything from /home/.config. Maybe only /home/.dillo and the downloads directory would be suitable?
Absolutely, that was my initial intention, but just wanted to keep the example patch as simple as possible. There are a number of things in $HOME which we probably don't want the browser having access to.
+ if (pledge("stdio rpath wpath cpath inet unix dns tty proc prot_exec",
Does this work with plugins, when the dpid daemon is not running?, as I believe it has to fork and exec the dpid program.
I started with a mindset of "whats the bare minimum of permissions we can get away with". But its clear that we would need "exec" as well for full functionality. At some point I may try to submit an improved patch to the OpenBSD ports maintainers. Unfortunately that won't do much for users of Linux and other systems. -Alex
Hi Alex, On Sat, Jul 20, 2024 at 09:03:48PM +0200, a1ex@dismail.de wrote:
Hi Rodrigo,
On Sat, 20 Jul 2024 16:34:47 +0200 Rodrigo Arias <rodarima@gmail.com> wrote:
+ if (unveil("/home", "rwc") == -1) {
We may want to constraint this a bit further, so a malicious actor cannot read anything from /home/.config. Maybe only /home/.dillo and the downloads directory would be suitable?
Absolutely, that was my initial intention, but just wanted to keep the example patch as simple as possible. There are a number of things in $HOME which we probably don't want the browser having access to.
Ups, I meant $HOME, not /home :-) There is a dGethomedir() function in dlib/dlib.h to help with this, which also works with other systems.
+ if (pledge("stdio rpath wpath cpath inet unix dns tty proc prot_exec",
Does this work with plugins, when the dpid daemon is not running?, as I believe it has to fork and exec the dpid program.
I started with a mindset of "whats the bare minimum of permissions we can get away with". But its clear that we would need "exec" as well for full functionality.
I can see clear benefits on restricting the file system access, but do you have in mind which type of attack the pledge() configuration would you help mitigate? I think allowing exec and inet is too permissive and would allow an attacker to easily spawn a remote shell as soon as a RCE bug is found. Maybe we can remove exec by spawning first the dpid daemon and then restricting the exec syscall with pledge. Currently it is only started when a request to a dpi plugin via a_Dpi_ccc().
At some point I may try to submit an improved patch to the OpenBSD ports maintainers. Unfortunately that won't do much for users of Linux and other systems.
I think is a good idea more OpenBSD people review this part, as I'm not very familiar with pledge(2). However, I'm afraid having this patch only in OpenBSD may cause a negative effect, as we may start receiving bug reports that are only happening on OpenBSD with that patch and cannot be reproduced on Linux or other systems without it. Maybe you can make a hardened Dillo port, so people can still test the non-hardened port to determine if the problem comes from these changes. This could be also controlled by a dillorc option (Dillo shouldn't be able to write dillorc), so it can be disabled for testing. The latter could be applied to upstream Dillo if we allow compiling the pledge part conditionally and would benefit from the CI tests on BSD too. Best, Rodrigo.
Hi Rodrigo, On Sun, 21 Jul 2024 13:33:32 +0200 Rodrigo Arias <rodarima@gmail.com> wrote:
I can see clear benefits on restricting the file system access, but do you have in mind which type of attack the pledge() configuration would you help mitigate?
It might reduce the attack surface for exploits in things like image and compression libraries. Or maybe there could be a bug in FLTK. Or in Dillo itself. I'm not saying these things are very likely, but they are possible. Since Dillo is a complicated program, we have to grant it access to lot of syscalls, which limits how effective pledge can be. But it still offers a level of protection which can be valuable in various scenerios.
I think allowing exec and inet is too permissive and would allow an attacker to easily spawn a remote shell as soon as a RCE bug is found.
I agree about exec, but right now without inet Dillo fails to start. One drawback to this current implementation is that things like my external link handler patch, and your multiple-actions patch don't work anymore. I ran some tests and confirmed that it causes issues, even with exec pledged. Maybe it would be possible to work around that with an un-pledged process which does the call, which would have to be started by Dillo prior to the pledge call in main. I did some experiments with initializing dpid prior to the pledge call being made, so that it can run un-pledged. It seems possible, but may require other changes which seem to be above my current skill level.
I think is a good idea more OpenBSD people review this part, as I'm not very familiar with pledge(2).
However, I'm afraid having this patch only in OpenBSD may cause a negative effect, as we may start receiving bug reports that are only happening on OpenBSD with that patch and cannot be reproduced on Linux or other systems without it.
Maybe you can make a hardened Dillo port, so people can still test the non-hardened port to determine if the problem comes from these changes.
I will have to think a bit on that, but maybe I'll start by forwarding this thread to some OpenBSD people and see what kind of response comes back. Really not sure how much interest there will be in this, as I suspect the amount of developers who use Dillo on OpenBSD is not huge. Hopefully I'm wrong. :) Regarding something you said earlier:
I believe it would be nice to move the network facing code away from the parsers, so the parser code cannot use the network or read the file system. This requires separating them in different processes.
This sounds like a very reasonable approach which would benefit all Dillo users and not just the small minority who have access to pledge and unveil. -Alex
Hi Alex, On Sun, Jul 21, 2024 at 07:31:29PM +0200, a1ex@dismail.de wrote:
Hi Rodrigo,
On Sun, 21 Jul 2024 13:33:32 +0200 Rodrigo Arias <rodarima@gmail.com> wrote:
I can see clear benefits on restricting the file system access, but do you have in mind which type of attack the pledge() configuration would you help mitigate?
It might reduce the attack surface for exploits in things like image and compression libraries. Or maybe there could be a bug in FLTK. Or in Dillo itself. I'm not saying these things are very likely, but they are possible. Since Dillo is a complicated program, we have to grant it access to lot of syscalls, which limits how effective pledge can be. But it still offers a level of protection which can be valuable in various scenerios.
Sorry, I think I didn't explained myself very clearly :-) With "which type of attack the pledge() configuration would you help mitigate?", I meant: if using your current configuration of "stdio rpath wpath cpath inet unix dns tty proc prot_exec" you have in mind an specific attack that we can mitigate, as this is giving Dillo access to a lot of syscalls which can be used to easily exploit any bugs. Also, I think it is very likely we have some exploitable bugs laying around in the parsers and other parts of the code and I don't think we will be able to use pledge effectively until we change the design to be able to split Dillo in more than one process. But maybe there is some exploitation that we can prevent right now with that combination that I'm not aware of. Otherwise I'm not sure if it is worth using pledge(2) as-is, until we change Dillo design so it is more effective. However, I think using unveil(2) is an effective way of restricting a lot of attack surface and doesn't require any design change. Maybe it would be better to start with unveil(2) alone and think later how to approach pledge(2). In Linux there is Landlock[1] (and also a project to implement an API like unveil on top of Landlock[2]), so it should be possible to at least implement support for OpenBSD and Linux to restrict access to the FS. [1]: https://landlock.io/ [2]: https://github.com/marty1885/landlock-unveil
Regarding something you said earlier:
I believe it would be nice to move the network facing code away from the parsers, so the parser code cannot use the network or read the file system. This requires separating them in different processes.
This sounds like a very reasonable approach which would benefit all Dillo users and not just the small minority who have access to pledge and unveil.
This is something I'm considering addressing already in RFC-002, as we will have to change the way Dillo exchanges data internally, we can move to a message passing design. While right now we will continue to use a single process, this design could potentially run over different processes, and then we could use pledge(2), seccomp(2) or other OS protection mechanisms for the untrusted code. I think it is important to remember that Dillo is a *multiplatform* browser, and it would be nice if security features are be available in all supported platforms. If you want to add support for unveil(2), I recommend you do that in the Dillo git repository with an #ifdef guard, so it can be enabled only if build for OpenBSD, and with some way to enable/disable it for testing. I can help you with this part if you are not familiar with autoconf. Then we can see how to add similar protection for other operating systems. Best, Rodrigo.
participants (2)
-
a1ex@dismail.de
-
Rodrigo Arias