Sandboxing a PDF Reader


Here is a short technical note about sandboxing a PDF reader (Okular) with Firejail.

Firejail is a Linux sandbox using the namespaces and seccomp-bpf features of the Linux kernel.

In my opinion, Okular is feature-wise the best PDF reader so that is what I'll use here; however, the instructions can be extended to apply to other readers such as Evince or Acrobat Reader.

The goals:

  • Okular can only see the PDF it is reading, nothing else.
  • Okular will not be able to reach the network.
  • The network will not be able to reach Okular.
  • Okular will not be able to see other processes.
  • Be able to open PDFs via the Nautilus file manager (Ubuntu).

The motivation for this is to reduce the attack surface, should a hostile PDF be opened.

What you need

Mise en place, you'll need:

The Firejail profile

To sandbox an application in Firejail you need a profile. There are built-in profiles, but let's make a custom one.

Replace YOURUSER with your username, some paths must be absolute.

I use a profile like this:

################################
# Generic GUI application profile
################################
include /etc/firejail/disable-mgmt.inc
include /etc/firejail/disable-secret.inc
include /etc/firejail/disable-common.inc
blacklist ${HOME}/.pki/nssdb
caps.drop all
seccomp
protocol unix,inet,inet6
netfilter
noroot

# Okular stuff starts here
read-only /etc
read-only /dev
read-only /boot
read-only /usr
read-only /opt
read-only /bin
read-only /sbin
read-only /lib
read-only /lib32
read-only /lib64
read-only /var
read-only /media
read-only /root
read-only ${HOME}

noblacklist ${HOME}/.kde/share/config/okularpartrc
whitelist ${HOME}/.kde/share/config/okularpartrc
read-only ${HOME}/.kde/share/config/okularpartrc
# cache-* depends on the host name
noblacklist ${HOME}/.kde/cache-okular
whitelist ${HOME}/.kde/cache-okular
whitelist ${HOME}/.kde
whitelist /tmp/.X11-unix/X0

private-dev
private-tmp

# the fonts stuff is needed for proper display of PDFs
private-etc passwd,groups,fonts/fonts.conf,fonts/conf.d,fonts/conf.avail

# no ${HOME} supported here apparently
netfilter /home/YOURUSER/.config/firejail/network-isolate.net

tracelog

hostname okular
name okular

Basically a lot of paths are set as read-only and the Okular configuration, a cache and the X11 socket are specifically whitelisted. The latter is problematic, and is a hole in the application isolation. It's possible to isolate X11 applications too, but that is not described here.

Custom network filtering is also set up. The "net none" directive caused problems with X11 connections.

The contents of the network isolation script (called network-isolate.net) are as follows:

$ cat /home/YOURUSER/.config/firejail/network-isolate.net
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]

-A INPUT -i lo -j DROP
-A OUTPUT -j DROP
-A FORWARD -j DROP
COMMIT

It just drops all incoming and outgoing traffic. Note that the X11 socket was specifically whitelisted in the profile.

Test this setup

A simple test can be done by running a shell such as Bash instead of Okular.

Note how Firejail is specifically told the profile to use, and just one file is whitelisted (as read-only). In the example, ~/Stuff contains a lot of other files:

YOURUSER@NSA-mainframe:~/Stuff$ ls -1
10.1.1.12.1241.pdf
10.1.1.512.8777.pdf
10.1.1.536.8043.pdf
YOURUSER@NSA-mainframe:~/Stuff$

The funny names are straight from Citeseer. Here is what the Firejail test looks like with the --trace enabled:

YOURUSER@NSA-mainframe:~/Stuff$ firejail --trace --profile=/home/YOURUSER/.config/firejail/okular.profile --whitelist=`readlink -f 10.1.1.536.8043.pdf` --read-only=`readlink -f 10.1.1.536.8043` bash
Reading profile /home/YOURUSER/.config/firejail/okular.profile
Reading profile /etc/firejail/disable-mgmt.inc
Reading profile /etc/firejail/disable-secret.inc
Reading profile /etc/firejail/disable-common.inc
Warning: --trace and --tracelog are mutually exclusive; --tracelog disabled
Parent pid 8232, child pid 8233

Child process initialized
11:bash:open /dev/tty:3
11:bash:open /dev/tty:3
11:bash:fopen /etc/passwd:0xaa5808
11:bash:open /etc/bash.bashrc:-1
11:bash:open /home/YOURUSER/.bashrc:-1
11:bash:open /home/YOURUSER/.bash_history:-1
11:bash:access /lib/terminfo/x/xterm-256color:0
11:bash:fopen /lib/terminfo/x/xterm-256color:0xab3808

# List the files, only the whitelisted file is seen among the trace output:

[YOURUSER@okular Stuff]$ ls -1
12:ls:fopen64 /proc/filesystems:0xc62010
12:ls:opendir .:0xc67c10
10.1.1.536.8043.pdf

# Try the network:

[YOURUSER@okular Stuff]$ nslookup google.com
13:nslookup:socket AF_INET SOCK_STREAM IPPROTO_IP:3
13:nslookup:socket AF_INET6 SOCK_STREAM IPPROTO_IP:3
13:nslookup:socket AF_LOCAL SOCK_STREAM IPPROTO_IP:3
13:nslookup:fopen64 /usr/lib/ssl/openssl.cnf:(nil)
13:nslookup:fopen64 /etc/resolv.conf:(nil)
13:nslookup:socket AF_INET SOCK_DGRAM IPPROTO_UDP:6
13:nslookup:bind 0.0.0.0 port 0:0
13:nslookup:socket AF_INET6 SOCK_DGRAM IPPROTO_UDP:6
13:nslookup:bind :::0
;; connection timed out; no servers could be reached

[YOURUSER@okular Stuff]$ exit
11:bash:open /home/YOURUSER/.bash_history:-1
11:bash:open /home/YOURUSER/.bash_history:-1
11:bash:open /home/YOURUSER/.bash_history:-1

parent is shutting down, bye...

System-wide setup

You need to be root for this.

It's not possible to specify variable parts like the invoked file in the profile, so we'll make a wrapper instead.

$ mv /usr/bin/okular /usr/bin/okular.original
$ vim /usr/bin/okular
$ ls -l /usr/bin/okular*
-rwxr-xr-x 1 root root    97 Feb 20 19:38 /usr/bin/okular
-rwxr-xr-x 1 root root 90752 Aug  5  2014 /usr/bin/okular.original
$ cat /usr/bin/okular
#!/bin/bash

# first expand to fully qualified filename
REAL_FILE=`readlink -f "$1"`

firejail --profile=/home/YOURUSER/.config/firejail/okular.profile \
   --whitelist="$REAL_FILE" --read-only="$REAL_FILE" okular.original "$REAL_FILE"

The file given as parameter is first expanded to a fully qualified absolute name, then whitelisted.

(Note that Firejail prohibits certain characters from the filename, you can easily disable or change this filter in function invalid_filename in the file util.c)

Now you might ask: "wait, we blocked the network, so can I still read documents from a network share?". That is a good question, and the answer is yes, you can. Generally, you just see a filesystem and another process takes care of reading/writing to the network share - the network stuff is not done by the Okular process.

Nautilus integration

Integration into Nautilus is very easy. Note, the "somefile.pdf" does not need to exist:

$ mimeopen -d somefile.pdf
Please choose a default application for files of type application/pdf

        1) Document Viewer  (evince)
        2) GIMP Image Editor  (gimp)
        3) Print Preview  (evince-previewer)
        4) Other...

use application #4
use command: okular

Now when right-clicking in Nautilus, the default selection is "okular" (the sandboxed one).

Gains and losses

What was gained?

Let's revisit the goals:

  • Okular can only see the PDF it is reading, nothing else. Yes!
  • Okular will not be able to reach the network. Yes!
  • The network will not be able to reach Okular. Yes!
  • Okular will not be able to see other processes. Yes!
  • Be able to open PDFs via the Nautilus file manager (Ubuntu). Yes!

Another note: one can still click on a link inside a PDF and open the link in a web browser; this is conducted via the DBus and may or may not be what you want.

What was lost?

One downside is that only one Okular instance can be open at a time. This is because the PID namespacing (CLONE_NEWPID) causes the PIDs to start from a low number inside the sandbox, and two different instances end up eventually registering themselves to DBus with the same PID, causing the latter registration to fail, and thus leading to the application stopping.

I'm not yet sure what would be the best way to work around this, perhaps by creating a full-blown (lxc) container for each Okular instance. This way each Okular would have their own DBus. Or perhaps one could run separate DBus instances without a full OS container.