Avoiding Harmful Code in Views with Liquid Templates

Problem

Contributed by: Christian Romney

You want to give your application's designers or end users the ability to design robust view templates without risking the security or integrity of your application.

Solution

Liquid templates are a popular alternative to the default ERb views with .rhtml templates. Liquid templates can't execute arbitrary code, so you can rest easy knowing your users won't accidentally destroy your database.

To install Liquid, you need the plug-in, but first you must tell Rails about its repository. From a console window in the Rails application's root directory type:

$ ruby script/plugin source svn://home.leetsoft.com/liquid/trunk
$ ruby script/plugin install liquid

Once the command has completed, you can begin creating Liquid templates. Like ERb, Liquid templates belong in the controller's folder under app/views. To create an index template for a controller named BlogController, for instance, you create a file named index.liquid in the folder.

Now, let's have a look at the Liquid markup syntax. To output some text, simply embed a string between a pair of curly braces:

{{ 'Hello, world!' }}

You can also pipe text through a filter using a syntax very similar to the Unix command line:

{{ 'Hello, world!' | downcase }}

All but the most trivial templates will need to include some logic, as well. Liquid includes support for conditional statements:

{% if user.last_name == 'Orsini' %}
 {{ 'Welcome back, Rob.' }}
{% endif %}

and for loops:

{% for line_item in order %}
 {{ line_item }}
{% endfor %}

Now for a complete example. Assume you've got an empty Rails application ready, with your database.yml file configured properly, and the Liquid plug-in installed as described above.

First, generate a model called Post:

$ ruby script/generate model Post

Next, edit the migration file: 001_create_posts.rb. For this example, you want to keep things simple:

db/migrate/001_create_posts.rb:

class CreatePosts < ActiveRecord::Migration
 def self.up
 create_table :posts do |t|
 t.column :title, :string
 end
 end
 def self.down
 drop_table :posts
 end end

Now, generate the database table by running:

$ rake db:migrate

With the posts table created, it's time to generate a controller for the application. Do this with:

$ ruby script/generate controller Posts

Now you're ready to add Liquid support to the application. Start your preferred development server with:

$ ruby script/server -d

Next, add some general support for rendering liquid templates within the application. Open the ApplicationController class file in your editor, and add the following render_liquid_template method:

app/controllers/application.rb:

class ApplicationController < ActionController::Base
 def render_liquid_template(options={}) 
 controller = options[:controller].to_s if options[:controller]
 controller ||= request.symbolized_path_parameters[:controller]
 action = options[:action].to_s if options[:action] 
 action ||= request.symbolized_path_parameters[:action]
 locals = options[:locals] || {} 
 locals.each_pair do |var, obj| 
 assigns[var.to_s] = \
 obj.respond_to?(:to_liquid) ? obj.to_liquid : obj 
 end
 path = "#{RAILS_ROOT}/app/views/#{controller}/#{action}.liquid"
 contents = File.read(Pathname.new(path).cleanpath)
 template = Liquid::Template.parse(contents) 
 returning template.render(assigns, :registers => {:controller => controller}) do |result|
 yield template, result if block_given?
 end
 end end

This method, which is partly based on code found in the excellent Mephisto publishing tool, finds the correct template to render, parses it in the context of the variables assigned, and is rendered when the application layout yields control to the index.liquid template.

To call this method, add the following index action to the PostsController:

app/controllers/posts_controller.rb:

class PostsController < ApplicationController
 def index
 @post = Post.new(:title => 'My First Post')
 render_liquid_template :locals => {:post => @post}
 end
 # ...
end

For convenience, add a simple to_liquid method to the Post model:

app/models/post.rb:

class Post < ActiveRecord::Base
 def to_liquid
 attributes.stringify_keys
 end end

You're just about finished. Next, you must create an index.liquid file in the app/views/posts directory. This template simply contains:

app/views/posts/index.liquid:

<h2>{{ post.title | upcase }}</h2>

Lastly, a demonstration of how you can even mix and match RHTML templates for your layout with Liquid templates for your views:

app/views/layouts/application.rhtml:

<html>
 <head>
 <title>Liquid Demo</title>
 </head>
 <body>
 <%= yield %>
 </body>
</html>

You're finally ready to view your application. Point your browser to ; e.g., posts.

Discussion

The main difference between Liquid and ERb is that Liquid doesn't use Ruby's Kernel#eval method when processing instructions. As a result, Liquid templates can process only data that is explicitly exposed to them, resulting in enhanced security. The Liquid templating language is also smaller than Ruby, arguably making it easier to learn in one sitting.

Liquid templates are also highly customizable. You can add your own text filters easily. Here's a simple filter that performs ROT-13 scrambling on a string:

module TextFilter
 def crypt(input)
 alpha = ('a'..'z').to_a.join 
 alpha += alpha.upcase
 rot13 = ('n'..'z').to_a.join + ('a'..'m').to_a.join
 rot13 += rot13.upcase
 input.tr(alpha, rot13)
 end end

To use this filter in your Liquid templates, create a folder called liquid_filters in the lib directory. In this new directory, add a file called text_filter.rb containing the code listed above.

Now open your environment.rb and enter:

config/environment.rb:

require 'liquid_filters/text_filter'
Liquid::Template.register_filter(TextFilter)

Your template could now include a line such as this one:

{{ post.title | crypt }}

Liquid is production-ready code. Tobias Lütke created Liquid to use on Shopify.com, an e-commerce tool for nonprogrammers. It's a very flexible and elegant tool and is usable by designers and end users alike. In practice, you'll probably want to cache your processed templates, possibly in the database. For a great example of Liquid templates in action, download the code for the Mephisto blogging tool from .

See Also