← Home

Sexy Theme Templating with Haml Safemode! Finally ...

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?