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?