Any functions that can open a file without cl:open

Discussion of Common Lisp
Post Reply
Jasper
Posts: 209
Joined: Fri Oct 10, 2008 8:22 am
Location: Eindhoven, The Netherlands
Contact:

Any functions that can open a file without cl:open

Post by Jasper » Wed Apr 07, 2010 7:17 am

The reason i ask is i am wondering i could limit lisp by limiting the packages, variables and functions allowed. I also want to keep the same functions to open files. Can files be opened without being JUDGED in the following:

Code: Select all

(defun judge (filename)
  (assert (not(char= (aref (namestring filename) 0) #\/)) nil
         "JUDGED you may not open files from the root.")
  (apply #'open opener)) 

(macrolet ((open (file &rest stuff)
             (declare (ignore stuff))
             `(judge ,file ,@stuff))
 ..judged part..)
Was thinking of combining this with a scan of the expressions used to (dis)allow certain functions/variables.

This would be useful for transferring xml lisp-style, with data represented in functions,(you could allow defun to prevent data-repeat) the disallowing to make it secure. It would also need limits on loops, nestedness of recursion, for that though..

It might also be useful to be able to 'fake' files sometimes by letting the judger make up it's own stream.

Also, how do you turn off the lock for an expression?

Edit: sockets are a security threat too, CL doesn't contain any of that, does it? In the case the file uses a lib with that, one would want a similar transparent limitation.

ramarren
Posts: 613
Joined: Sun Jun 29, 2008 4:02 am
Location: Warsaw, Poland
Contact:

Re: Any functions that can open a file without cl:open

Post by ramarren » Wed Apr 07, 2010 8:14 am

Just from the top of my mind, DRIBBLE will open and write to file. And there are also various INTERN/read macros tricks that can be used to avoid this kind of sandboxing. Not to mention the foreign function interface and other implementation extensions.

The only at least somewhat secure way to execute code of uncertain validity would be to write an interpreter/compiler for the subset of the language you want. Whitelists are always more secure than blacklists.
Jasper wrote:Also, how do you turn off the lock for an expression?
I'm assuming you mean package locks, which are implementation dependent. The only thing the standard says about redefining the meaning of standard symbols is that is is undefined. For SBCL they are described in the manual.

Jasper
Posts: 209
Joined: Fri Oct 10, 2008 8:22 am
Location: Eindhoven, The Netherlands
Contact:

Re: Any functions that can open a file without cl:open

Post by Jasper » Wed Apr 07, 2010 10:17 am

Good point.. you're right i should focus on a whitelist then. And it will need a guard on intern too. The following scans the code on symbols and packages, and can both blacklist as whitelist at symbol and package-level.(It is pretty straightforward)

Code: Select all

(defpackage :code-guard
  (:use :common-lisp)
  (:export trust-p thrust-these trust-expr-p)
  (:documentation "Tells you whether to trust expressions, allowing only\
 certain symbols and packages."))

(in-package :code-guard)

(defvar *trust* (make-hash-table :test 'equalp)
  "Trusted symbols/packages, WARNING: nil is maybe, :no is definitely not,\
 :yes is definitely yes, it is not t/nil")

(defun trust-p (in)
  "Whether something is trusted."
  (the (or null (eql :yes) (eql :no))
    (typecase in
    ;These and before refers to packages.
      (keyword (trust-p (find-package in)))
      (string  (gethash in *trust*))
      (package (trust-p (package-name in)))
    ;This one to symbols in general.
      (symbol  (or (gethash in *trust*)
                   (trust-p (symbol-package in))))
      (t       (error "I don't do this: ~s" objs)))))

(defun (setf trust-p) (to in)
  "Set whether something is trusted."
  (declare (type (or null (eql :yes) (eql :no))))
  (typecase in
    (keyword (setf (trust-p (find-package in)) to))
    (string  (setf (gethash in *trust*) to))
    (package (setf (trust-p (package-name in)) to))
    (symbol  (setf (gethash in *trust*) to))
    (t       (error "I don't do this: ~s" objs))))

(defmacro trust-these (way &rest to-trust)
  (dolist (el to-trust) (setf (trust-p el) way)))

(defun trust-expr-p (expr)
  "Whether and expression is trusted."
  (typecase expr
    (null
     :yes)
    (symbol
     (trust-p expr))
    (list
     (if (find-if-not (lambda (e) (eql :yes (trust-expr-p e))) expr)
       :no :yes))
    ((or string number complex character)
     :yes)
    (t
     (error "Didn't recognize ~s and should be suspicious." expr))))

(trust-these :yes + - * / if when let lambda defun defmacro)
Problem is that it is hard to figure out if it is hackable..(I could easily write a unit-test, but it wouldn't guarantee anything) Hmm, perhaps i should add the distinction between function-like and variable-like use and use expression-hook, that involves expanding a lot, but i guess it can return the expanded aswel. Reader macros should be a problem if i read it myself and only allow the basics.

When it works, you only have to worry that the whitelisted symbols are safe, though. Cffi and such should have their own packages, are off the whitelist that way. Any functions accessing files/using the network. And stuff using untrusted things might need them too, if that is a prerequisite of white-listing them.

So so far the functions that need extra guards when trusted are OPEN, INTERN. I don't think i'll allow DRIBBLE, then. (Anyone use it?) EVAL, DEFMACRO might be a good ones to kick out.. That removes a lot of possible constructions. I guess for more i'll have to iterate over the symbols.

ramarren
Posts: 613
Joined: Sun Jun 29, 2008 4:02 am
Location: Warsaw, Poland
Contact:

Re: Any functions that can open a file without cl:open

Post by ramarren » Wed Apr 07, 2010 10:47 am

What exactly are your security considerations? Stopping file access might be possible, but stopping code from taking all memory/cpu time is much harder without building a proper virtual machine. In general, trying to turn an environment which is not designed to be such into a sandox will not work against a determined attacker. On the other hand, if determined attacks are unlikely and this is mostly meant as a sanity check, then your approach might work.

Also about read macros: there are some perhaps not obvious things there, for example: #99999999A() will send SBCL straight into ldb with heap exhaustion.

gugamilare
Posts: 406
Joined: Sat Mar 07, 2009 6:17 pm
Location: Brazil
Contact:

Re: Any functions that can open a file without cl:open

Post by gugamilare » Wed Apr 07, 2010 11:27 am

Ramarren wrote:Also about read macros: there are some perhaps not obvious things there, for example: #99999999A() will send SBCL straight into ldb with heap exhaustion.
Read macros are not needed for this, consider, for instance:

Code: Select all

(loop collect t)
This will kill your PC in less than one minute, unless, e.g., you limit SBCL's heap size, in which case the ldb is entered.

Code: Select all

gugamilare@gugamilare-desktop:~$ sbcl --dynamic-space-size 100
This is SBCL 1.0.36, an implementation of ANSI Common Lisp.   
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the      
distribution for more information.                                 
* (loop collect t)                                                 
Heap exhausted during garbage collection: 0 bytes available, 16 requested.
 Gen StaPg UbSta LaSta LUbSt Boxed Unboxed LB   LUB  !move  Alloc  Waste   Trig    WP  GCs Mem-age
   0: 20048     0     0     0  6147     0     0     0     0 25174048  4064 14591072    0   1  0,0000
   1: 25599     0     0     0  8534     3     0     0    13 34952640 14912  2000000 2970   0  0,3494
   2:     0     0     0     0     0     0     0     0     0        0     0  2000000    0   0  0,0000
   3:     0     0     0     0     0     0     0     0     0        0     0  2000000    0   0  0,0000
   4:     0     0     0     0     0     0     0     0     0        0     0  2000000    0   0  0,0000
   5:     0     0     0     0     0     0     0     0     0        0     0  2000000    0   0  0,0000
   6:     0     0     0     0  9759  1157     0     0     0 44711936     0  2000000 9723   0  0,0000
   Total bytes allocated    = 104838624
   Dynamic-space-size bytes = 104857600
GC control variables:
          *GC-INHIBIT* = true
          *GC-PENDING* = in progress
 *STOP-FOR-GC-PENDING* = false
fatal error encountered in SBCL pid 2240(tid 140737353987824):
Heap exhausted, game over.

Welcome to LDB, a low-level debugger for the Lisp runtime environment.
ldb>
I liked the last sentence :lol:

ramarren
Posts: 613
Joined: Sun Jun 29, 2008 4:02 am
Location: Warsaw, Poland
Contact:

Re: Any functions that can open a file without cl:open

Post by ramarren » Wed Apr 07, 2010 11:50 am

gugamilare wrote:Read macros are not needed for this, consider, for instance:
Of course, but this is done by the execution of code you presumably already don't trust, and might make attempts to stop things like that. But, it is tempting to use the reader to read that code after disabling *READ-EVAL* and such, and it still won't help. Which means that to even moderately safely execute untrusted code you need both a custom reader and a custom virtual machine.

Jasper
Posts: 209
Joined: Fri Oct 10, 2008 8:22 am
Location: Eindhoven, The Netherlands
Contact:

Re: Any functions that can open a file without cl:open

Post by Jasper » Wed Apr 07, 2010 12:28 pm

Read macros at this point aren't an issue at all. At this point i am only considering the s-expressions. Getting their textual representation to be secure doesn't sound like a very hard thing to do.

Well the idea is that people would be able to make web-applications* with some sort a browser for it, and users don't have to bother with trusting the source too much, because it is boxed in. That, of course, is pretty much the highest level of desired security one want! The upside is that the most limited level would just be a bunch of functions without side effects being allowed to only depend on each other with no cycles, and so also no way to loop.(But still might consume too much memory.. from what i've heard Haskell wouldn't have that problem) That would pretty much be the 'perfect xml' :p (As you might would do making a parser yourself) So i think it can be useful without that level of security.

And then some would hopefully use GIL :), but the idea applies more generally.

But this is indeed sounding like a very hard thing to do. Didn't think that hard about macros like LOOP, could of course add a little counter to it too. (Although adding to a macro would be trickier. And a non-limited DEFUN would need guarding too. Would be neater if one could just give a thread limits in cpu*time and memory.

I also realize now that i must ensure nothing gets into the variables *, **, ***, to be exploited, probably a LET over the box should do it if they're just special variables. Alternatively i could use expression-hook to distinguish between allowing variables and functions, but that complicates having to trust the output of the macros too. Anything the macro expands to has to either be allowed, or you have to use def-base-macro to make it ignore the insides if any of it is not trustable in other contexts. And expression-hook is again a bunch of code that might have weaknesses.

*I don't like having stuff on the web you can't easily download and run all-on-your-own-machine, though.

gugamilare
Posts: 406
Joined: Sat Mar 07, 2009 6:17 pm
Location: Brazil
Contact:

Re: Any functions that can open a file without cl:open

Post by gugamilare » Wed Apr 07, 2010 1:16 pm

One thing you could try to do to avoid freezing the lisp implementation (and your program) is to start a new process with lisp, e.g.:

Code: Select all

(sb-ext:run-program "/usr/local/bin/sbcl" '("--load" "virtual-machine-starter.lisp"))
and communicate with this process. If the external process freezes, have a heap exhaustion, enters an eternal loop, etc, the main process will still be alive and can easily kill the old lisp and start a new one. You will still need to be careful with many other problems.

By the way, there are other ways to access the root of the file system. Consider paths like "../../../":

Code: Select all

cl-user> (directory #p"../../../")
(#P"/")
cl-user> (namestring #p"../../../")
"../../../" ;; it does not start with a /
cl-user> (pathname-directory #p"../../../")
(:relative :up :up :up) ;; it is also not :absolute
So:

Code: Select all

(defun judge (filename)
  (assert (not (intersection '(:absolute :up)
                             (pathname-directory (pathname filename)))))
  (apply #'open opener)) 

Jasper
Posts: 209
Joined: Fri Oct 10, 2008 8:22 am
Location: Eindhoven, The Netherlands
Contact:

Re: Any functions that can open a file without cl:open

Post by Jasper » Fri Apr 09, 2010 11:22 am

Thanks for the help!

I formalized the guarding:

Code: Select all

(defvar *guards* nil "List of guards.")

(defun guard-name (name)
  (declare (type symbol name))
  (intern (concatenate 'string "GUARD-" (symbol-name name))))

(defmacro def-guard (name (&rest args) &body body)
  "Defines a guard onto a function. Essentially you redefine the \
function/macro see if anything malicious can be done with it, and\
 disallowing it if so. (It must also be clear what sort of things are\
 allowed.)"
  (with-gensyms (form)
    `(flet ((alarm (datum &rest arguments)
	      "Function to call if the guards finds something it\
 distrusts.)"
	      (apply #'error (cons datum arguments))
	      (values))
	    (guard-assert (test-form datum &rest arguments)
	      (when test-form
		(apply #'alarm (cons datum arguments)))))
       (pushnew ',name *guards*)
       (defun ,(guard-name name) (&rest ,form)
	 (destructuring-bind (,@args) ,form ,@body)))))

(defmacro guarded (() &body body)
  "Checks if the symbols are allowed, and executes body with guards"
  (dolist (expr body)
    (multiple-value-bind (trust-p distrust) (trust-expr-p expr)
      (unless (eql trust-p :yes)
	(error "Don't trust the body. Distrust ~s" distrust))))
  `(macrolet
       (,@(mapcan
	   (lambda (name)
	     (with-gensyms (args)
	       (when (macro-function name)
		 `((,name (&rest ,args)
		     (apply (function ,(guard-name name)) ,args))))))
	   *guards*))
     (flet (,@(mapcan
	       (lambda (name)
		 (unless (macro-function name)
		   `((,name (&rest args)
		      (apply (function ,(guard-name name)) args)))))
	       *guards*))
       ,@body)))
Then i started with guards for Intern, Funcall, and Apply.

Code: Select all

(def-guard intern (name &optional (package *package*))
  "Guarding symbols, important because functions call be funcalled thusly"
  (if (trust-p package)
    (intern name package)
    (let ((result (intern name package)))
      (if (trust-p result)
	result
	(alarm "Disallowed symbol ~s" result)))))

(def-guard apply (fun args)
  (typecase fun
    (symbol
     (assert (trust-p fun) nil "BUG: symbol found here should already have\
 been noticed as not being trusted!")
     (if (find fun *guards*) ;But this is really what it is about:
       (apply (guard-name fun) args) fun))
    (function
     (apply fun args))))

(def-guard funcall (fun &rest args)
  (guard-apply fun args))
As you should be able to test, without the guards on funcall and apply you can hack it:(On sbcl, disable the lock with sb:ext-unlock-package, weirdly enough sb-ext:disable-package-locks doesn't seem to work.)

Code: Select all

(trust-these :yes funcall apply intern)
(guarded ()
  (funcall (intern "INTERN") "HAX")) ;Where Hax is not allowed as an symbol.
Apparently Funcall/Apply don't pick the local function when fed a symbol.

Haven't gotten to filesystem stuff yet, for that i'd like to allow/disallow directories/files(I mean, you can allow a directory and then disallow some subdirectory/files in there) and that needs some sort of decent representation. I have no idea how many guards will be needed for a decent subset of CL, just what is mentioned now, and probably more in obscure functions, although Funcall and Apply already surprised me.. Anything making variables will probably also require a guard if i am going to allow stuff like *default-pathname-defaults*. (Probably will need a 'var-guard')

Note that the code in OP is a little older, than current but i checked that it works together if you just add Alexandria to the :use'd packages. I pushed the stuff in the lisp-editor git it is in 'tools/' might be better to have this in a separate project, though. (should i make one?)

We could play 'guard and sneak past the guard' though :). Making guards is easy enough, certainty of being secure doesn't seem very easy.. Dare you to sneak past Funcall, Apply and Intern guards! Edit: you may presume other symbols allowed, of course! (needless to say, or make a guard to sneak by)

Post Reply