From c3e6cde67f4af0604f2e8e14cc747eb7bcb5b844 Mon Sep 17 00:00:00 2001 From: Jason Roelofs Date: Wed, 16 Jan 2013 09:45:55 -0500 Subject: [PATCH 1/7] Add security tests to show that the symbol table doesn't grow --- test/liquid/security_test.rb | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/liquid/security_test.rb b/test/liquid/security_test.rb index 413cdf6b..0e9ac334 100644 --- a/test/liquid/security_test.rb +++ b/test/liquid/security_test.rb @@ -38,4 +38,27 @@ class SecurityTest < Test::Unit::TestCase assert_equal expected, Template.parse(text).render(@assigns, :filters => SecurityFilter) end + + def test_does_not_add_filters_to_symbol_table + current_symbols = Symbol.all_symbols + + test = %( {{ "some_string" | a_bad_filter }} ) + + template = Template.parse(test) + assert_equal [], (Symbol.all_symbols - current_symbols) + + template.render + assert_equal [], (Symbol.all_symbols - current_symbols) + end + + def test_does_not_add_drop_methods_to_symbol_table + current_symbols = Symbol.all_symbols + + drop = Drop.new + drop.invoke_drop("custom_method_1") + drop.invoke_drop("custom_method_2") + drop.invoke_drop("custom_method_3") + + assert_equal [], (Symbol.all_symbols - current_symbols) + end end # SecurityTest From 7bcb565668a17cbdd08dedba60dfc972a00c1f63 Mon Sep 17 00:00:00 2001 From: Jason Roelofs Date: Tue, 15 Jan 2013 11:33:17 -0500 Subject: [PATCH 2/7] Remove #to_sym calls from Drop and Variable Symbols are not needed here and using plain strings is nicer on Ruby --- lib/liquid/drop.rb | 4 ++-- lib/liquid/variable.rb | 2 +- test/liquid/variable_test.rb | 30 +++++++++++++++--------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/liquid/drop.rb b/lib/liquid/drop.rb index ee2a4d71..4d415c8c 100644 --- a/lib/liquid/drop.rb +++ b/lib/liquid/drop.rb @@ -31,8 +31,8 @@ module Liquid # called by liquid to invoke a drop def invoke_drop(method_or_key) - if method_or_key && method_or_key != EMPTY_STRING && self.class.public_method_defined?(method_or_key.to_s.to_sym) - send(method_or_key.to_s.to_sym) + if method_or_key && method_or_key != EMPTY_STRING && self.class.public_method_defined?(method_or_key.to_s) + send(method_or_key.to_s) else before_method(method_or_key) end diff --git a/lib/liquid/variable.rb b/lib/liquid/variable.rb index 0c04fe28..962a9927 100644 --- a/lib/liquid/variable.rb +++ b/lib/liquid/variable.rb @@ -26,7 +26,7 @@ module Liquid if matches = f.match(/\s*(\w+)/) filtername = matches[1] filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/o).flatten - @filters << [filtername.to_sym, filterargs] + @filters << [filtername, filterargs] end end end diff --git a/test/liquid/variable_test.rb b/test/liquid/variable_test.rb index 7bdef35b..1e3335bf 100644 --- a/test/liquid/variable_test.rb +++ b/test/liquid/variable_test.rb @@ -11,71 +11,71 @@ class VariableTest < Test::Unit::TestCase def test_filters var = Variable.new('hello | textileze') assert_equal 'hello', var.name - assert_equal [[:textileze,[]]], var.filters + assert_equal [["textileze",[]]], var.filters var = Variable.new('hello | textileze | paragraph') assert_equal 'hello', var.name - assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters + assert_equal [["textileze",[]], ["paragraph",[]]], var.filters var = Variable.new(%! hello | strftime: '%Y'!) assert_equal 'hello', var.name - assert_equal [[:strftime,["'%Y'"]]], var.filters + assert_equal [["strftime",["'%Y'"]]], var.filters var = Variable.new(%! 'typo' | link_to: 'Typo', true !) assert_equal %!'typo'!, var.name - assert_equal [[:link_to,["'Typo'", "true"]]], var.filters + assert_equal [["link_to",["'Typo'", "true"]]], var.filters var = Variable.new(%! 'typo' | link_to: 'Typo', false !) assert_equal %!'typo'!, var.name - assert_equal [[:link_to,["'Typo'", "false"]]], var.filters + assert_equal [["link_to",["'Typo'", "false"]]], var.filters var = Variable.new(%! 'foo' | repeat: 3 !) assert_equal %!'foo'!, var.name - assert_equal [[:repeat,["3"]]], var.filters + assert_equal [["repeat",["3"]]], var.filters var = Variable.new(%! 'foo' | repeat: 3, 3 !) assert_equal %!'foo'!, var.name - assert_equal [[:repeat,["3","3"]]], var.filters + assert_equal [["repeat",["3","3"]]], var.filters var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !) assert_equal %!'foo'!, var.name - assert_equal [[:repeat,["3","3","3"]]], var.filters + assert_equal [["repeat",["3","3","3"]]], var.filters var = Variable.new(%! hello | strftime: '%Y, okay?'!) assert_equal 'hello', var.name - assert_equal [[:strftime,["'%Y, okay?'"]]], var.filters + assert_equal [["strftime",["'%Y, okay?'"]]], var.filters var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!) assert_equal 'hello', var.name - assert_equal [[:things,["\"%Y, okay?\"","'the other one'"]]], var.filters + assert_equal [["things",["\"%Y, okay?\"","'the other one'"]]], var.filters end def test_filter_with_date_parameter var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!) assert_equal "'2006-06-06'", var.name - assert_equal [[:date,["\"%m/%d/%Y\""]]], var.filters + assert_equal [["date",["\"%m/%d/%Y\""]]], var.filters end def test_filters_without_whitespace var = Variable.new('hello | textileze | paragraph') assert_equal 'hello', var.name - assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters + assert_equal [["textileze",[]], ["paragraph",[]]], var.filters var = Variable.new('hello|textileze|paragraph') assert_equal 'hello', var.name - assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters + assert_equal [["textileze",[]], ["paragraph",[]]], var.filters var = Variable.new("hello|replace:'foo','bar'|textileze") assert_equal 'hello', var.name - assert_equal [[:replace, ["'foo'", "'bar'"]], [:textileze, []]], var.filters + assert_equal [["replace", ["'foo'", "'bar'"]], ["textileze", []]], var.filters end def test_symbol var = Variable.new("http://disney.com/logo.gif | image: 'med' ") assert_equal 'http://disney.com/logo.gif', var.name - assert_equal [[:image,["'med'"]]], var.filters + assert_equal [["image",["'med'"]]], var.filters end def test_string_single_quoted From a48e162237ae50e72dc7f9c3ae125372eea1fbea Mon Sep 17 00:00:00 2001 From: Jason Roelofs Date: Wed, 16 Jan 2013 10:20:01 -0500 Subject: [PATCH 3/7] Change Drop method lookup to not hit respond_to? Class.public_method_defined? ends up diving into Ruby's core looking for a method with the given method_or_key. This process at some point turns method_or_key into a Symbol. This change no longer takes that path and thus doesn't grow the Symbol table. --- lib/liquid/drop.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/liquid/drop.rb b/lib/liquid/drop.rb index 4d415c8c..3972d08d 100644 --- a/lib/liquid/drop.rb +++ b/lib/liquid/drop.rb @@ -31,7 +31,7 @@ module Liquid # called by liquid to invoke a drop def invoke_drop(method_or_key) - if method_or_key && method_or_key != EMPTY_STRING && self.class.public_method_defined?(method_or_key.to_s) + if method_or_key && method_or_key != EMPTY_STRING && drop_method_defined?(method_or_key.to_s) send(method_or_key.to_s) else before_method(method_or_key) @@ -47,5 +47,12 @@ module Liquid end alias :[] :invoke_drop + + private + + # Check for method existence without invoking respond_to?, which creates symbols + def drop_method_defined?(method_name) + self.class.public_instance_methods.any? {|method| method.to_s == method_name } + end end end From 1300210f05ee06ffda69365711556c7f5ed4276d Mon Sep 17 00:00:00 2001 From: Jason Roelofs Date: Wed, 16 Jan 2013 11:14:01 -0500 Subject: [PATCH 4/7] Convert Strainer to white-list method protection After moving the method existence check from Context into Strainer, updated Strainer to only accept invokation methods that were added via filter Modules, and done in a way that respond_to? is never called, preventing unconstrained Symbol table growth. --- lib/liquid/context.rb | 6 +--- lib/liquid/strainer.rb | 38 +++++++++++----------- test/liquid/context_test.rb | 6 ++-- test/liquid/strainer_test.rb | 62 ++++++++++++++++++++++++++++-------- 4 files changed, 70 insertions(+), 42 deletions(-) diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index 4500aeff..e334677a 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -71,11 +71,7 @@ module Liquid end def invoke(method, *args) - if strainer.respond_to?(method) - strainer.__send__(method, *args) - else - args.first - end + strainer.invoke(method, *args) end # Push new local scope on the stack. use Context#stack instead diff --git a/lib/liquid/strainer.rb b/lib/liquid/strainer.rb index 445a0aea..79f9ca7e 100644 --- a/lib/liquid/strainer.rb +++ b/lib/liquid/strainer.rb @@ -9,16 +9,11 @@ module Liquid end # Strainer is the parent class for the filters system. - # New filters are mixed into the strainer class which is then instanciated for each liquid template render run. + # New filters are mixed into the strainer class which is then instantiated for each liquid template render run. # - # One of the strainer's responsibilities is to keep malicious method calls out + # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, + # Context#add_filters or Template.register_filter class Strainer < parent_object #:nodoc: - INTERNAL_METHOD = /^__/ - @@required_methods = Set.new([:__id__, :__send__, :respond_to?, :kind_of?, :extend, :methods, :singleton_methods, :class, :object_id]) - - # Ruby 1.9.2 introduces Object#respond_to_missing?, which is invoked by Object#respond_to? - @@required_methods << :respond_to_missing? if Object.respond_to? :respond_to_missing? - @@filters = {} def initialize(context) @@ -36,19 +31,22 @@ module Liquid strainer end - def respond_to?(method, include_private = false) - method_name = method.to_s - return false if method_name =~ INTERNAL_METHOD - return false if @@required_methods.include?(method_name) - super - end - - # remove all standard methods from the bucket so circumvent security - # problems - instance_methods.each do |m| - unless @@required_methods.include?(m.to_sym) - undef_method m + def invoke(method, *args) + if has_method?(method) + send(method, *args) + else + args.first end end + + private + + def has_method?(method) + methods_to_check = self.methods - self.class.public_instance_methods + methods_to_check.any? do |instance_method| + instance_method.to_s == method.to_s + end + end + end end diff --git a/test/liquid/context_test.rb b/test/liquid/context_test.rb index c2c316f1..e50b2373 100644 --- a/test/liquid/context_test.rb +++ b/test/liquid/context_test.rb @@ -189,10 +189,10 @@ class ContextTest < Test::Unit::TestCase end context = Context.new - methods_before = context.strainer.methods.map { |method| method.to_s } + assert_equal "Wookie", context.invoke("hi", "Wookie") + context.add_filters(filter) - methods_after = context.strainer.methods.map { |method| method.to_s } - assert_equal (methods_before + ["hi"]).sort, methods_after.sort + assert_equal "Wookie hi!", context.invoke("hi", "Wookie") end def test_add_item_in_outer_scope diff --git a/test/liquid/strainer_test.rb b/test/liquid/strainer_test.rb index fc92e10d..7e759f79 100644 --- a/test/liquid/strainer_test.rb +++ b/test/liquid/strainer_test.rb @@ -3,23 +3,57 @@ require 'test_helper' class StrainerTest < Test::Unit::TestCase include Liquid - def test_strainer - strainer = Strainer.create(nil) - assert_equal false, strainer.respond_to?('__test__') - assert_equal false, strainer.respond_to?('test') - assert_equal false, strainer.respond_to?('instance_eval') - assert_equal false, strainer.respond_to?('__send__') - assert_equal true, strainer.respond_to?('size') # from the standard lib + module AccessScopeFilters + def public_filter + "public" + end + + def private_filter + "private" + end + private :private_filter end - def test_should_respond_to_two_parameters - strainer = Strainer.create(nil) - assert_equal true, strainer.respond_to?('size', false) + module TestingFilter + def test1 + "test1" + end + + def test2 + "test2" + end end - # Asserts that Object#respond_to_missing? is not being undefined in Ruby versions where it has been implemented - # Currently this method is only present in Ruby v1.9.2, or higher - def test_object_respond_to_missing - assert_equal Object.respond_to?(:respond_to_missing?), Strainer.create(nil).respond_to?(:respond_to_missing?) + Strainer.global_filter(AccessScopeFilters) + Strainer.global_filter(TestingFilter) + + def test_strainer_only_invokes_public_filter_methods + strainer = Strainer.create(nil) + assert_equal "public", strainer.invoke("public_filter") end + + def test_strainer_returns_nil_if_no_filter_method_found + strainer = Strainer.create(nil) + assert_nil strainer.invoke("private_filter") + assert_nil strainer.invoke("undef_the_filter") + end + + def test_strainer_returns_first_argument_if_no_method_and_arguments_given + strainer = Strainer.create(nil) + assert_equal "password", strainer.invoke("undef_the_method", "password") + end + + def test_strainer_allows_multiple_filters + strainer = Strainer.create(nil) + assert_equal "test1", strainer.invoke("test1") + assert_equal "test2", strainer.invoke("test2") + end + + def test_strainer_only_allows_methods_defined_in_filters + strainer = Strainer.create(nil) + assert_equal "1 + 1", strainer.invoke("instance_eval", "1 + 1") + assert_equal "puts", strainer.invoke("__send__", "puts", "Hi Mom") + assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke") + end + end # StrainerTest From 38b4543bf184cb2fcae743528d6a9b39c20bbd32 Mon Sep 17 00:00:00 2001 From: Dylan Smith Date: Tue, 5 Feb 2013 14:45:08 -0500 Subject: [PATCH 5/7] Use sets to check if methods are invokable without symbolizing. --- lib/liquid/context.rb | 1 + lib/liquid/drop.rb | 11 +++++++---- lib/liquid/strainer.rb | 33 +++++++++++++++++---------------- test/liquid/drop_test.rb | 7 +++++++ test/liquid/strainer_test.rb | 29 +++++++++++------------------ 5 files changed, 43 insertions(+), 38 deletions(-) diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index e334677a..129b71a8 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -39,6 +39,7 @@ module Liquid filters.each do |f| raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module) + Strainer.add_known_filter(f) strainer.extend(f) end end diff --git a/lib/liquid/drop.rb b/lib/liquid/drop.rb index 3972d08d..88adb6be 100644 --- a/lib/liquid/drop.rb +++ b/lib/liquid/drop.rb @@ -1,3 +1,5 @@ +require 'set' + module Liquid # A drop in liquid is a class which allows you to export DOM like things to liquid. @@ -31,8 +33,8 @@ module Liquid # called by liquid to invoke a drop def invoke_drop(method_or_key) - if method_or_key && method_or_key != EMPTY_STRING && drop_method_defined?(method_or_key.to_s) - send(method_or_key.to_s) + if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key) + send(method_or_key) else before_method(method_or_key) end @@ -51,8 +53,9 @@ module Liquid private # Check for method existence without invoking respond_to?, which creates symbols - def drop_method_defined?(method_name) - self.class.public_instance_methods.any? {|method| method.to_s == method_name } + def self.invokable?(method_name) + @invokable_methods ||= Set.new((public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s)) + @invokable_methods.include?(method_name.to_s) end end end diff --git a/lib/liquid/strainer.rb b/lib/liquid/strainer.rb index 79f9ca7e..15aefa48 100644 --- a/lib/liquid/strainer.rb +++ b/lib/liquid/strainer.rb @@ -2,19 +2,15 @@ require 'set' module Liquid - parent_object = if defined? BlankObject - BlankObject - else - Object - end - # Strainer is the parent class for the filters system. # New filters are mixed into the strainer class which is then instantiated for each liquid template render run. # # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, # Context#add_filters or Template.register_filter - class Strainer < parent_object #:nodoc: + class Strainer #:nodoc: @@filters = {} + @@known_filters = Set.new + @@known_methods = Set.new def initialize(context) @context = context @@ -22,9 +18,20 @@ module Liquid def self.global_filter(filter) raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module) + add_known_filter(filter) @@filters[filter.name] = filter end + def self.add_known_filter(filter) + unless @@known_filters.include?(filter) + @@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s)) + new_methods = filter.instance_methods.map(&:to_s) + new_methods.reject!{ |m| @@method_blacklist.include?(m) } + @@known_methods.merge(new_methods) + @@known_filters.add(filter) + end + end + def self.create(context) strainer = Strainer.new(context) @@filters.each { |k,m| strainer.extend(m) } @@ -32,21 +39,15 @@ module Liquid end def invoke(method, *args) - if has_method?(method) + if invokable?(method) send(method, *args) else args.first end end - private - - def has_method?(method) - methods_to_check = self.methods - self.class.public_instance_methods - methods_to_check.any? do |instance_method| - instance_method.to_s == method.to_s - end + def invokable?(method) + @@known_methods.include?(method.to_s) && respond_to?(method) end - end end diff --git a/test/liquid/drop_test.rb b/test/liquid/drop_test.rb index ebac1e5e..7819f642 100644 --- a/test/liquid/drop_test.rb +++ b/test/liquid/drop_test.rb @@ -115,6 +115,13 @@ class DropsTest < Test::Unit::TestCase assert_equal ' ', output end + def test_object_methods_not_allowed + [:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method| + output = Liquid::Template.parse(" {{ product.#{method} }} ").render('product' => ProductDrop.new) + assert_equal ' ', output + end + end + def test_scope assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new) assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) diff --git a/test/liquid/strainer_test.rb b/test/liquid/strainer_test.rb index 7e759f79..582ed7f3 100644 --- a/test/liquid/strainer_test.rb +++ b/test/liquid/strainer_test.rb @@ -14,22 +14,21 @@ class StrainerTest < Test::Unit::TestCase private :private_filter end - module TestingFilter - def test1 - "test1" - end - - def test2 - "test2" - end - end - Strainer.global_filter(AccessScopeFilters) - Strainer.global_filter(TestingFilter) + + def test_strainer + strainer = Strainer.create(nil) + assert_equal 5, strainer.invoke('size', 'input') + assert_equal "public", strainer.invoke("public_filter") + end def test_strainer_only_invokes_public_filter_methods strainer = Strainer.create(nil) - assert_equal "public", strainer.invoke("public_filter") + assert_equal false, strainer.invokable?('__test__') + assert_equal false, strainer.invokable?('test') + assert_equal false, strainer.invokable?('instance_eval') + assert_equal false, strainer.invokable?('__send__') + assert_equal true, strainer.invokable?('size') # from the standard lib end def test_strainer_returns_nil_if_no_filter_method_found @@ -43,12 +42,6 @@ class StrainerTest < Test::Unit::TestCase assert_equal "password", strainer.invoke("undef_the_method", "password") end - def test_strainer_allows_multiple_filters - strainer = Strainer.create(nil) - assert_equal "test1", strainer.invoke("test1") - assert_equal "test2", strainer.invoke("test2") - end - def test_strainer_only_allows_methods_defined_in_filters strainer = Strainer.create(nil) assert_equal "1 + 1", strainer.invoke("instance_eval", "1 + 1") From d706db3bd70ace02d5cf4d91fe57d7208e312a2f Mon Sep 17 00:00:00 2001 From: Dylan Smith Date: Thu, 28 Feb 2013 16:49:07 -0500 Subject: [PATCH 6/7] Add support for filter keyword arguments. Closes #175 --- lib/liquid/variable.rb | 15 +++++++++++---- test/liquid/filter_test.rb | 13 +++++++++++++ test/liquid/variable_test.rb | 6 ++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/liquid/variable.rb b/lib/liquid/variable.rb index 0c04fe28..2c317a81 100644 --- a/lib/liquid/variable.rb +++ b/lib/liquid/variable.rb @@ -23,9 +23,9 @@ module Liquid if match[2].match(/#{FilterSeparator}\s*(.*)/o) filters = Regexp.last_match(1).scan(FilterParser) filters.each do |f| - if matches = f.match(/\s*(\w+)/) + if matches = f.match(/\s*(\w+)(?:\s*#{FilterArgumentSeparator}(.*))?/) filtername = matches[1] - filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/o).flatten + filterargs = matches[2].to_s.scan(/(?:\A|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten @filters << [filtername.to_sym, filterargs] end end @@ -36,9 +36,16 @@ module Liquid def render(context) return '' if @name.nil? @filters.inject(context[@name]) do |output, filter| - filterargs = filter[1].to_a.collect do |a| - context[a] + filterargs = [] + keyword_args = {} + filter[1].to_a.each do |a| + if matches = a.match(/\A#{TagAttributes}\z/o) + keyword_args[matches[1]] = context[matches[2]] + else + filterargs << context[a] + end end + filterargs << keyword_args unless keyword_args.empty? begin output = context.invoke(filter[0], output, *filterargs) rescue FilterNotFound diff --git a/test/liquid/filter_test.rb b/test/liquid/filter_test.rb index f24a70c5..d924c6a1 100644 --- a/test/liquid/filter_test.rb +++ b/test/liquid/filter_test.rb @@ -16,6 +16,12 @@ module CanadianMoneyFilter end end +module SubstituteFilter + def substitute(input, params={}) + input.gsub(/%\{(\w+)\}/) { |match| params[$1] } + end +end + class FiltersTest < Test::Unit::TestCase include Liquid @@ -92,6 +98,13 @@ class FiltersTest < Test::Unit::TestCase assert_equal 1000, Variable.new("var | xyzzy").render(@context) end + + def test_filter_with_keyword_arguments + @context['surname'] = 'john' + @context.add_filters(SubstituteFilter) + output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context) + assert_equal 'hello john, doe', output + end end class FiltersInTemplate < Test::Unit::TestCase diff --git a/test/liquid/variable_test.rb b/test/liquid/variable_test.rb index 7bdef35b..58a1ee39 100644 --- a/test/liquid/variable_test.rb +++ b/test/liquid/variable_test.rb @@ -107,6 +107,12 @@ class VariableTest < Test::Unit::TestCase var = Variable.new(%| test.test |) assert_equal 'test.test', var.name end + + def test_filter_with_keyword_arguments + var = Variable.new(%! hello | things: greeting: "world", farewell: 'goodbye'!) + assert_equal 'hello', var.name + assert_equal [[:things,["greeting: \"world\"","farewell: 'goodbye'"]]], var.filters + end end From f7d1e1d0c1336a9c51d652f07e0187b429e154e4 Mon Sep 17 00:00:00 2001 From: Dylan Smith Date: Tue, 5 Mar 2013 16:46:15 -0500 Subject: [PATCH 7/7] Release 2.5.0 --- History.md | 8 ++++++++ liquid.gemspec | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index e19fef97..09a4eae9 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,13 @@ # Liquid Version History +## 2.5.0 / 2013-03-06 + +* Prevent Object methods from being called on drops +* Avoid symbol injection from liquid +* Added break and continue statements +* Fix filter parser for args without space separators +* Add support for filter keyword arguments + ## 2.4.0 / 2012-08-03 * Performance improvements diff --git a/liquid.gemspec b/liquid.gemspec index 91d4b946..29ac4ea2 100644 --- a/liquid.gemspec +++ b/liquid.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |s| s.name = "liquid" - s.version = "2.4.1" + s.version = "2.5.0" s.platform = Gem::Platform::RUBY s.summary = "A secure, non-evaling end user template engine with aesthetic markup." s.authors = ["Tobias Luetke"]