Sexy Theme Templating with Haml Safemode! Finally ...

posted: February 5th, 2008 · by: Sven

in: Programming · tagged as: , , , , ·  8 comments »

Ok, this is really a looong lasting itch of mine I wanted to scratch ever since I’ve learned Liquid templates for Mephisto.

Liquid still is (as far as I know) the only usable “safe” Ruby templating engine that one could use for themes/templates in an application like Mephisto. In this context “safe” means that you can allow your users to download and install themes from arbitrary sources.

Liquid is safe …

So, with Liquid you can still sleep at night without any worries that some bastard might have included a bit of code into a theme that sends your password files to the russian mafia, runs rm -rf / or whatever nightmare you like worse.

Liquid does a very solid job here and as such it earns respect. But … let’s face it: Liquid sucks, syntaxwise.

As a Ruby programmer you want a templating system that makes your templates easier to type and more intuitive to grasp than ERB, not worse! Maybe it’s really just me, but for me Liquid fails miserably in this regard.

Haml is sexy …

On the other side of the Ruby template engines universe lives Haml. A templating system that is that awesome that you can’t possibly toy around with it for more than 3 minutes without getting totally addicted to it. But Haml is an evaluating templating system like ERB and as such you can’t use it for themes from arbitrary sources.

So how cool would it be to combine the best of both? Obviously it’d totally rock. It would be as cool as Yahoo sunglasses in 1994 and as sexy as the Audi R8 in 2008 combined.

But that would be really hard, wouldn’t it?

I always thought that implementing this would be way over my head. To accomplish this one had to parse the Ruby code that Haml evaluates and take measures to ensure that only certain (whitelisted) methods on assigned objects can be called at all.

It’s easy to limit the template author’s access to certain methods on our own stuff. Liquid greatly demonstrates how to do this with its so called Drops and Strainers.

But how can one make sure that nothing else is used besides the objects assigned to the template? I.e. how could we prevent a (valid) Haml snippet like = File.open('/etc/htpasswd'){|f| f.read} from being evaluated?

RubyParser rocks …

Last night I accidentally stumbled across the RubyParser which is actually a Ruby syntax parser written in Ruby. And it’s available as a gem! Yeah. So with RubyParser we can easily hack Haml to parse and check any Ruby code from the templates before storing it for later evaluation.

My impression is that (given that we’ve closed access to unsecure methods down for the assigned variables) all we have to do is forbid access to all Ruby constants (because suspicious methods like Kernel.load, File.read etc. are all on classes) and shell command execution using backticks.

I’m sure that I’m missing something here … and I’d very much appreciate your heads-up if you see anything that also needs to be forbidden to make this waterproof.

But I’m totally thrilled that there’s an approach to make Haml a real candidate for a theme template engine finally. Oh, and, of course this can be applied to other evaluating templating engines like ERB, too. :)

I plan to continue playing with this and then check back with the Haml folks whether this could make its way into Haml or release it as a plugin for Haml otherwise.

For those of you interested in actual code … here’s a proof of concept piece of code:


require 'rubygems'
require 'haml'
require 'ruby_parser'

class Object
  def to_jail
    Haml::Jail.new self
  end
end

module Haml
  class SafeModeError < RuntimeError; end

  class Jail
    attr_reader :source    
    def initialize(source)
      @source = source
    end

    def method_missing(method, *args)
      # could easily hook in a whitelisted approach for allowing access to 
      # certain methods here
      warn "calling #{method} on #{source.class.name}... allow this?"
      Jail.new @source.send(method, *args)
    end

    def to_s
      @source.to_s
    end

    def to_jail
      self
    end
  end    
end

Haml::Engine.class_eval do
  alias_method :render_without_jailed_locals, :render

  def render(scope = Object.new, locals = {}, &block)
    locals = jail_all(locals) if options[:safemode]
    render_without_jailed_locals(scope, locals, &block)
  end

  def jail_all(vars)
    Hash[*vars.collect{|name, value| [name, value.to_jail]}.flatten]
  end
end

Haml::Precompiler.class_eval do
  alias_method :push_script_without_safeguard, :push_script

  def push_script(text, flattened, close_tag = nil)
    flush_merged_text
    return if options[:suppress_eval]
    safeguard_script(text.strip) if options[:safemode]
    push_script_without_safeguard(text, flattened, close_tag)
  rescue Haml::SafeModeError => error
    warn error.message
  end

  def safeguard_script(code)
    nodes = RubyParser.new.parse(code).to_a.flatten
    # do we need to forbit anything else then access to constants 
    # and shell command backticks?
    if nodes.include?(:const)
      raise Haml::SafeModeError, "Safemode doesn't allow access to constants."
    elsif nodes.include?(:xstr)
      raise Haml::SafeModeError, "Safemode doesn't allow shell command execs."
    end
  end
end

template = <<EOC
%p I can access methods on locals
%p
  = 'piece of evaluated %s code' % lang.downcase.strip
%p I can interate:
%p 
  - (1..3).each do |i|
    = i
%p and I can branch:
%p 
  - if true
    Yep!
  - else
    Nope ... :(
%p But I can't access constants ...
= File.open('/etc/passwd'){|f| f.read}
%p ... or execute shell commands
= `ls -a`
EOC
haml = Haml::Engine.new(template, {:safemode => true})
puts haml.render(Object.new, :lang => 'ruby')  

This will output:


<p>I can access local stuff</p>
<p>
  piece of evaluated ruby code
</p>
<p>I can interate:</p>
<p>
  1
  2
  3
</p>
<p>and I can branch:</p>
<p>
  Yep!
</p>
<p>But I can't access constants ...</p>
<p>... or execute shell commands</p>
Safemode doesn't allow to access constants.
Safemode doesn't allow shell command execution.
calling downcase on String... allow this?
calling strip on String... allow this?

Leave a comment

8 Comments

  1. Peter Cooper said February 5th, 2008 at 10:26 PM  

    Nice try, but it’s far from that easy! Try:

    = system(‘touch /tmp/helloworld’)

    It works.

    Or what about:

    = (eval “Kernel”).load(‘whatever’)

    :)

  2. Sven said February 5th, 2008 at 10:39 PM  

    Hi Peter,

    thanks for the hints!

    Well, obviously this isn’t sufficient. But I believe it might be possible … and that’s what I’m so excited about :)

    Those lines you name produce the following trees:

    p RubyParser.new.parse("system('touch /tmp/helloworld')")
    s(:fcall, :system, s(:array, s(:str, "touch /tmp/helloworld")))
    
    p RubyParser.new.parse("(eval 'Kernel').load('whatever')")
    s(:call, s(:fcall, :eval, s(:array, s(:str, "Kernel"))), :load, 
    s(:array, s(:str, "whatever")))
    

    Both contain the :fcall token which obviously is something that should (and easily can) be forbidden, too.

    Unfortunately I haven’t been able to find any documentation for those node type names that RubyParser spits out. The RubyParser source might have some additional hints here.

    Do you happen to know more examples like these?

    Thanks again!

  3. Peter Cooper said February 5th, 2008 at 11:05 PM  

    (Your Mephisto is crashing when I send my full post, so I am hacking about till it posts.. this may mean my examples no longer work or may have extra spaces in them!)

    The reason I know about some of these is I’ve worked with Why’s “Sandbox” quite a bit and tried to make my own, and there are all sorts of horrible things you can do :( I am not so familiar with the internals RubyParser is providing access to, which makes this quite an interesting exercise, and one I might have a play with!

    Another nasty example:

    • self.freeze
  4. Peter Cooper said February 5th, 2008 at 11:06 PM  

    How about?

    = 5.send(:PUT A BACKTICK HERE, ‘ls’)

    Or a classic from C Erler:

    = $stdout. class.for_fd($stdout.class.sysopen(‘/ etc/ passwd’)). read(4096)

    You might find this old thread useful:

    http://blade.nagaokaut.ac.jp/cgi-bin/vframe.rb/ruby/ruby-core/6604?6473-6830+split-mode-vertical

    If you are preventing the use of constants, you are actually solving a reasonable amount of the problem in a public sense but the gigantic warren hiding in the background is quite imposing.

  5. Sven said February 5th, 2008 at 11:18 PM  

    Mephisto is crashing? Woha!

    Would you mind to send me your full post by email so I can fix this?

    Also, I’d love to see your examples … maybe we just continue this discussion by email? I’m still more comfortable with my email client than a blog comment textarea ;)

    You’re right that for a real sandbox way more measurements might be necessary. (I’ve also looked at Why’s sandbox once, but obviously one can’t rely on it for a common templating engine … not yet at least?)

    For a template system I believe it’s easier because we can hook into the template locals/variables assignment process and whitelist allowed methods there (that’s what Liquid does and I think that should be adaptable).

    Also, something like self.freeze could be turned off by having Haml evaluate the code inside of a restrictive Object like Rails’ blankslate?

  6. Ryan Davis said February 7th, 2008 at 12:32 AM  

    Noooooooooooooooooooooooooooooooooooooooooooooo!

  7. Sven said February 7th, 2008 at 09:10 AM  

    Hi Ryan!

    LOL … I almost spoiled my coffee over the keyboard.

    Nooooo?

    So that means you’re less thrilled with this idea I guess. Mind to explain that a bit?

  8. Sven said February 7th, 2008 at 09:46 AM  

    Peter, your second comment was waiting for moderation and I missed that, sorry.

    Yeah, 1.send(‘system’, ‘ls -a’) actually works … interesting.

    Thanks!

Leave a comment

Name required
E-Mail and Website optional

If you can read this, you don't use a typical webbrowser that plays nice with CSS.
Please do not fill in anything here!

Hint: Markdown will be applied to your comment. If you post any code, be sure to escape underscores (like so: \_) if you do not want them to be converted to an <em>phasis.

artweb design
Sven Fuchs
Grünberger Str. 65
10245 Berlin, Germany


http://www.artweb-design.de

Fon +49 (30) 47 98 69 96
Fax +49 (30) 47 98 69 97