a safer, more capable bot: IRC part 4

we need to protect our bot against attackers. crafting a custom environment allows us to expose just what we need and nothing more.

[with the bot code open from the last episode]

so in the last episode we had this chat bot which you could teach
facts that it would repeat back to you, and it stored the facts in the
data directory on disk. problem is it could also read and write
outside the data directory if you gave it the right input.

with a little more code we can restructure this so even if you forget
to sanitize your inputs, it still can't access the filesystem outside
where it should. the best part is we don't have to change the code.

[open main.fnl]

we're going to construct a custom environment and run our code inside
it. this is basically equivalent to running code with a new, isolated
set of globals, or a sandbox if you prefer.

our environment needs to be able to do I/O, so we give it print, plus

    (local env {:io {:read io.read : open} : print})

but instead of giving it io.open for files, we give it our
own version of open which wraps the real one.

    (fn open [filename mode]
      (assert (legal-file? filename) (.. "disallowed file: " filename))
      (io.open filename mode))

what constitutes a legal filename? well, it has to be start in the
data directory, and it can't leave it by using ..:

    (fn legal-file? [filename]
      (and (filename:match "^data/")
           (not (filename:match "%.%."))))

once we've constructed our environment, we need to use it:

    (local fennel (require :fennel))
    (fennel.dofile "bot.fnl" {: env})

there's one final step before we can run this; the environment needs
to be populated with a few other basic globals. outside of print and
io, these are all safe functions which can't leak any state out of
the environment; it's mostly just some error handling:

    (local env {:io {:read io.read : open} : print
                :package {:loaded {}} :debug {:traceback debug.traceback}
                : string : type : assert : error : xpcall : pcall})

and we're off! our original bot.fnl code stays the same, but we'll
launch netcat again pointed to our new entry point:

$ nc -c "fennel main.fnl" localhost 6667

let's teach our bot some things:

[switch back to #lodef]
> lodefbot: set programming to fun
> lodefbot: set netcat to useful
> lodefbot: what is netcat?
> lodefbot: set ../main.fnl to (steal-your "data")

haha, welp! we prevented illegal access, but we crashed the whole
thing. one tiny edit will make it more resilient. in a real program
we'd give some kind of message back, but for now, just avoiding a
crash is good enough.

    (while true
      (pcall handle (io.read)))

if you want to learn more about this style of isolating your code and
only passing in the capabilities it needs, I gave a whole talk on this
topic at FennelConf, see the link below. there's also a link to
pengbot, a fully-featured IRC bot which uses a more elaborate version
of this technique.

that's all I've got on this topic for now. thanks for watching, and
hope to see you again in future lodef episodes.