liquid-autoescape 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/LICENSE +21 -0
- data/README.md +165 -0
- data/lib/liquid/autoescape.rb +33 -0
- data/lib/liquid/autoescape/configuration.rb +41 -0
- data/lib/liquid/autoescape/core_exemptions.rb +40 -0
- data/lib/liquid/autoescape/errors.rb +11 -0
- data/lib/liquid/autoescape/exemption.rb +47 -0
- data/lib/liquid/autoescape/exemption_list.rb +106 -0
- data/lib/liquid/autoescape/filters.rb +26 -0
- data/lib/liquid/autoescape/liquid_ext/variable.rb +38 -0
- data/lib/liquid/autoescape/tags/autoescape.rb +45 -0
- data/lib/liquid/autoescape/template_variable.rb +74 -0
- data/lib/liquid/autoescape/version.rb +5 -0
- data/spec/functional/autoescape_tag_spec.rb +200 -0
- data/spec/unit/autoescape_spec.rb +49 -0
- data/spec/unit/configuration_spec.rb +72 -0
- data/spec/unit/core_exemptions_spec.rb +72 -0
- data/spec/unit/exemption_list_spec.rb +165 -0
- data/spec/unit/exemption_spec.rb +29 -0
- data/spec/unit/template_variable_spec.rb +80 -0
- metadata +148 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
require "liquid"
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
module Autoescape
|
5
|
+
|
6
|
+
# Liquid filters used to support the autoescape tag
|
7
|
+
module Filters
|
8
|
+
|
9
|
+
# Flag an input as exempt from autoescaping
|
10
|
+
#
|
11
|
+
# This is a non-transformative filter that works by registering itself
|
12
|
+
# in a variable's filter chain. If a variable detects this in its
|
13
|
+
# filters, no escaping will be performed on it.
|
14
|
+
#
|
15
|
+
# @param [String] input A variable's content
|
16
|
+
# @return [String] The unmodified content
|
17
|
+
def skip_escape(input)
|
18
|
+
input
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
Template.register_filter(Filters)
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "liquid"
|
2
|
+
require "liquid/autoescape"
|
3
|
+
require "liquid/autoescape/template_variable"
|
4
|
+
|
5
|
+
module Liquid
|
6
|
+
class Variable
|
7
|
+
|
8
|
+
# @private
|
9
|
+
alias_method :non_escaping_render, :render
|
10
|
+
|
11
|
+
# Possibly render the variable with HTML escaping applied
|
12
|
+
#
|
13
|
+
# If the autoescaping context variable has been set by the +autoescape+ tag
|
14
|
+
# or Liquid autoescaping is globally enabled, this will run the variable
|
15
|
+
# through the global exemption list to determine if it is exempt from
|
16
|
+
# autoescaping. If it is not, its contents will be rendered as a string
|
17
|
+
# with all unsafe HTML characters escaped. In all other cases, the
|
18
|
+
# original, unescaped value of the variable will be rendered.
|
19
|
+
#
|
20
|
+
# @param [Liquid::Context] context The variable's rendering context
|
21
|
+
# @return [String] The potentially escaped contents of the variable
|
22
|
+
def render(context)
|
23
|
+
if !Autoescape.configuration.global? && !context[Autoescape::ENABLED_FLAG]
|
24
|
+
return non_escaping_render(context)
|
25
|
+
end
|
26
|
+
|
27
|
+
variable = Autoescape::TemplateVariable.from_liquid_variable(self)
|
28
|
+
is_exempt = Autoescape.configuration.exemptions.apply?(variable)
|
29
|
+
|
30
|
+
@filters << [:escape, []] unless is_exempt
|
31
|
+
output = non_escaping_render(context)
|
32
|
+
@filters.pop unless is_exempt
|
33
|
+
|
34
|
+
output
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "liquid"
|
2
|
+
require "liquid/autoescape/liquid_ext/variable"
|
3
|
+
|
4
|
+
module Liquid
|
5
|
+
module Autoescape
|
6
|
+
module Tags
|
7
|
+
|
8
|
+
# A block tag that automatically escapes all variables contained within it
|
9
|
+
#
|
10
|
+
# All contained variables will have dangerous HTML characters escaped.
|
11
|
+
# Any variables that should be exempt from escaping should have the
|
12
|
+
# +skip_escape+ filter applied to them.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# {% assign untrusted = "<script>window.reload();</script>" %}
|
16
|
+
# {% assign trusted = "<strong>Text</strong>" %}
|
17
|
+
#
|
18
|
+
# {% autoescape %}
|
19
|
+
# {{ untrusted }}
|
20
|
+
# {{ trusted | skip_escape }}
|
21
|
+
# {% endautoescape %}
|
22
|
+
class Autoescape < Block
|
23
|
+
|
24
|
+
def initialize(tag_name, markup, tokens)
|
25
|
+
unless markup.empty?
|
26
|
+
raise SyntaxError, "Syntax Error in 'autoescape' - Valid syntax: {% autoescape %}"
|
27
|
+
end
|
28
|
+
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def render(context)
|
33
|
+
context.stack do
|
34
|
+
context[ENABLED_FLAG] = true
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
Template.register_tag("autoescape", Autoescape)
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Liquid
|
2
|
+
module Autoescape
|
3
|
+
|
4
|
+
# A wrapper around a Liquid variable used in a template
|
5
|
+
#
|
6
|
+
# This provides a consistent interface to a Liquid variable, accounting
|
7
|
+
# for structural differences in variables between different Liquid versions
|
8
|
+
# and exposing a simple list of applied filters. All exemptions are
|
9
|
+
# determined by examining instances of this object.
|
10
|
+
class TemplateVariable
|
11
|
+
|
12
|
+
# The name of the variable
|
13
|
+
#
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
# The names of the filters applied to the variable
|
18
|
+
#
|
19
|
+
# @return [Array<Symbol>]
|
20
|
+
attr_reader :filters
|
21
|
+
|
22
|
+
class << self
|
23
|
+
|
24
|
+
# Create a wrapper around a Liquid variable instance
|
25
|
+
#
|
26
|
+
# This normalizes the variable's information, since Liquid handles
|
27
|
+
# variable names differently across versions.
|
28
|
+
#
|
29
|
+
# @param [Liquid::Variable] variable A Liquid variable as used in a template
|
30
|
+
# @return [Liquid::Autoescape::TemplateVariable]
|
31
|
+
def from_liquid_variable(variable)
|
32
|
+
name = normalize_variable_name(variable)
|
33
|
+
filters = variable.filters.map { |f| f.first.to_sym }
|
34
|
+
|
35
|
+
new(:name => name, :filters => filters)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Normalize the name of a Liquid variable
|
41
|
+
#
|
42
|
+
# Liquid 2 exposes the full variable name directly on the
|
43
|
+
# +Liquid::Variable+ instance, while Liquid 3 manages it via a
|
44
|
+
# +Liquid::VariableLookup+ instance that tracks both the base name and
|
45
|
+
# any lookup paths involved.
|
46
|
+
#
|
47
|
+
# @param [Liquid::Variable] variable A Liquid variable as used in a template
|
48
|
+
# @return [String] The name of the Liquid variable
|
49
|
+
def normalize_variable_name(variable)
|
50
|
+
lookup_name = variable.name.instance_variable_get("@name")
|
51
|
+
return variable.name unless lookup_name
|
52
|
+
|
53
|
+
parts = [lookup_name]
|
54
|
+
variable.name.instance_variable_get("@lookups").each do |lookup|
|
55
|
+
parts << lookup
|
56
|
+
end
|
57
|
+
parts.join(".")
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
# Create a wrapper around a Liquid variable used in a template
|
63
|
+
#
|
64
|
+
# @options options [String] :name The name of the variable
|
65
|
+
# @options options [Array<Symbol>] :filters The filters applied to the variable
|
66
|
+
def initialize(options={})
|
67
|
+
@name = options.fetch(:name)
|
68
|
+
@filters = options[:filters] || []
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require "liquid/autoescape"
|
2
|
+
|
3
|
+
describe "{% autoescape %}" do
|
4
|
+
|
5
|
+
def verify_template_output(template, expected, context={})
|
6
|
+
rendered = Liquid::Template.parse(template).render!(context)
|
7
|
+
expect(rendered).to eq(expected)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "handles empty content" do
|
11
|
+
verify_template_output(
|
12
|
+
"{% autoescape %}{% endautoescape %}",
|
13
|
+
""
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "handles non-variable content" do
|
18
|
+
verify_template_output(
|
19
|
+
"{% autoescape %}Static{% endautoescape %}",
|
20
|
+
"Static"
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "escapes all dangerous HTML characters" do
|
25
|
+
verify_template_output(
|
26
|
+
"{% autoescape %}{{ variable }}{% endautoescape %}",
|
27
|
+
"<tag> & "quote"",
|
28
|
+
"variable" => '<tag> & "quote"'
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "applies HTML escaping to all variables inside the block tag" do
|
33
|
+
verify_template_output(
|
34
|
+
"{% autoescape %}{{ one }} {{ two }} {{ three }}{% endautoescape %}",
|
35
|
+
"<tag> & "quote"",
|
36
|
+
"one" => "<tag>", "two" => "&", "three" => '"quote"'
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "applies HTML escaping to a filtered variable" do
|
41
|
+
verify_template_output(
|
42
|
+
"{% autoescape %}{{ filtered | downcase | capitalize }}{% endautoescape %}",
|
43
|
+
"A <strong> tag",
|
44
|
+
"filtered" => "A <STRONG> Tag"
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "does not double-escape variables" do
|
49
|
+
verify_template_output(
|
50
|
+
"{% autoescape %}{{ escaped | escape }}{% endautoescape %}",
|
51
|
+
"HTML & CSS",
|
52
|
+
"escaped" => "HTML & CSS"
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "does not escape variables outside the block tag" do
|
57
|
+
verify_template_output(
|
58
|
+
"{{ variable }} {% autoescape %}{{ variable }}{% endautoescape %} {{ variable }}",
|
59
|
+
"& & &",
|
60
|
+
"variable" => "&"
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "can be called multiple times" do
|
65
|
+
verify_template_output(
|
66
|
+
"{% autoescape %}{{ var }}{% endautoescape %} {{ var }} {% autoescape %}{{ var }}{% endautoescape %}",
|
67
|
+
"& & &",
|
68
|
+
"var" => "&"
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "does not escape variables with the skip_escape filter applied" do
|
73
|
+
verify_template_output(
|
74
|
+
"{% autoescape %}{{ variable | skip_escape }}{% endautoescape %}",
|
75
|
+
"<strong>&</strong>",
|
76
|
+
"variable" => "<strong>&</strong>"
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "raises an error when called with arguments" do
|
81
|
+
invalid = "{% autoescape on %}{% endautoescape %}"
|
82
|
+
expect { Liquid::Template.parse(invalid) }.to raise_error(Liquid::SyntaxError)
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "configuration options" do
|
86
|
+
|
87
|
+
after(:each) { Liquid::Autoescape.reconfigure }
|
88
|
+
|
89
|
+
context "with global mode enabled" do
|
90
|
+
|
91
|
+
before(:each) do
|
92
|
+
Liquid::Autoescape.configure do |config|
|
93
|
+
config.global = true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
it "escapes variables outside of an autoescape block" do
|
98
|
+
verify_template_output(
|
99
|
+
"{{ variable }}",
|
100
|
+
"&",
|
101
|
+
"variable" => "&"
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "escapes variables in an autoescape block" do
|
106
|
+
verify_template_output(
|
107
|
+
"{% autoescape %}{{ variable }}{% endautoescape %}",
|
108
|
+
"&",
|
109
|
+
"variable" => "&"
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "respects escaping filters" do
|
114
|
+
verify_template_output(
|
115
|
+
"{{ variable | skip_escape }}{% autoescape %}{{ variable | skip_escape }}{% endautoescape %}",
|
116
|
+
"&&",
|
117
|
+
"variable" => "&"
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
context "with custom exemptions" do
|
124
|
+
|
125
|
+
let(:exemptions) do
|
126
|
+
Module.new do
|
127
|
+
def exemption(variable)
|
128
|
+
variable.name == "module"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
before(:each) do
|
134
|
+
Liquid::Autoescape.configure do |config|
|
135
|
+
config.exemptions.add { |variable| variable.name == "filter" }
|
136
|
+
config.exemptions.import(exemptions)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
it "does not escape exempt variables" do
|
141
|
+
verify_template_output(
|
142
|
+
"{% autoescape %}{{ filter }} {{ module }} {{ other }}{% endautoescape %}",
|
143
|
+
"<a> <b> <i>",
|
144
|
+
"filter" => "<a>", "module" => "<b>", "other" => "<i>"
|
145
|
+
)
|
146
|
+
end
|
147
|
+
|
148
|
+
it "can handle exemptions with lookup-style variable names" do
|
149
|
+
Liquid::Autoescape.configure do |config|
|
150
|
+
config.exemptions.add { |variable| variable.name == "root.one" }
|
151
|
+
config.exemptions.add { |variable| variable.name == "trunk.branch.leaf" }
|
152
|
+
end
|
153
|
+
|
154
|
+
verify_template_output(
|
155
|
+
"{% autoescape %}{{ root.one }} {{ root.two }} {{ trunk.branch.leaf }}{% endautoescape %}",
|
156
|
+
"<a> <b> <i>",
|
157
|
+
"root" => {"one" => "<a>", "two" => "<b>"},
|
158
|
+
"trunk" => {"branch" => {"leaf" => "<i>"}}
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "respects the default exemptions" do
|
163
|
+
verify_template_output(
|
164
|
+
"{% autoescape %}{{ filter | skip_escape }} {{ other }}{% endautoescape %}",
|
165
|
+
"<a> <b>",
|
166
|
+
"filter" => "<a>", "other" => "<b>"
|
167
|
+
)
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
context "with trusted filters" do
|
173
|
+
|
174
|
+
before(:each) do
|
175
|
+
Liquid::Autoescape.configure do |config|
|
176
|
+
config.trusted_filters << :downcase
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
it "does not forget the default escaping filters" do
|
181
|
+
verify_template_output(
|
182
|
+
"{% autoescape %}{{ one | skip_escape }} {{ two | escape }}{% endautoescape %}",
|
183
|
+
"<a> <b>",
|
184
|
+
"one" => "<a>", "two" => "<b>"
|
185
|
+
)
|
186
|
+
end
|
187
|
+
|
188
|
+
it "exempts variables that use one of the trusted filters" do
|
189
|
+
verify_template_output(
|
190
|
+
"{% autoescape %}{{ variable | downcase }} {{ variable | capitalize }}{% endautoescape %}",
|
191
|
+
"r&d R&d",
|
192
|
+
"variable" => "R&D"
|
193
|
+
)
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "liquid/autoescape"
|
2
|
+
require "liquid/autoescape/configuration"
|
3
|
+
|
4
|
+
module Liquid
|
5
|
+
describe Autoescape do
|
6
|
+
|
7
|
+
after(:each) { Autoescape.reconfigure }
|
8
|
+
|
9
|
+
describe ".configure" do
|
10
|
+
|
11
|
+
it "allows autoescape settings to be customized" do
|
12
|
+
Autoescape.configure do |config|
|
13
|
+
expect(config).to be_an_instance_of(Autoescape::Configuration)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
describe ".reconfigure" do
|
20
|
+
|
21
|
+
it "undoes any user configuration" do
|
22
|
+
Autoescape.configure do |config|
|
23
|
+
config.trusted_filters << :my_custom_filter
|
24
|
+
end
|
25
|
+
|
26
|
+
expect(Autoescape.configuration.trusted_filters).to include(:my_custom_filter)
|
27
|
+
|
28
|
+
Autoescape.reconfigure
|
29
|
+
expect(Autoescape.configuration.trusted_filters).to_not include(:my_custom_filter)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
describe ".configuration" do
|
35
|
+
|
36
|
+
it "exposes the current configuration object" do
|
37
|
+
Autoescape.configure do |config|
|
38
|
+
config.global = true
|
39
|
+
end
|
40
|
+
|
41
|
+
config = Autoescape.configuration
|
42
|
+
expect(config).to be_an_instance_of(Autoescape::Configuration)
|
43
|
+
expect(config.global?).to be(true)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "liquid/autoescape/configuration"
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
module Autoescape
|
5
|
+
describe Configuration do
|
6
|
+
|
7
|
+
let(:config) { Configuration.new }
|
8
|
+
|
9
|
+
describe "global mode" do
|
10
|
+
|
11
|
+
it "is off by default" do
|
12
|
+
expect(config.global?).to be(false)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "can be turned on" do
|
16
|
+
config.global = true
|
17
|
+
expect(config.global?).to be(true)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "the list of custom exemptions" do
|
23
|
+
|
24
|
+
it "has default exemptions" do
|
25
|
+
expect(config.exemptions.populated?).to be(true)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can accept custom exemption filters" do
|
29
|
+
expect { config.exemptions.add { true } }.to change { config.exemptions.size }.by(1)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "cannot directly exemption filters" do
|
33
|
+
exemption = lambda { true }
|
34
|
+
expect { config.exemptions << exemption }.to raise_error
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "the list of trusted filters" do
|
40
|
+
|
41
|
+
it "is empty by default" do
|
42
|
+
expect(config.trusted_filters).to be_empty
|
43
|
+
end
|
44
|
+
|
45
|
+
it "can receive additional filters" do
|
46
|
+
config.trusted_filters << :downcase
|
47
|
+
expect(config.trusted_filters).to match_array([:downcase])
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#reset" do
|
53
|
+
|
54
|
+
it "restores the default configuration values" do
|
55
|
+
config.global = true
|
56
|
+
config.exemptions.add { true }
|
57
|
+
config.trusted_filters << :downcase
|
58
|
+
|
59
|
+
expect(config.global?).to be(true)
|
60
|
+
expect(config.trusted_filters.size).to be(1)
|
61
|
+
|
62
|
+
expect { config.reset }.to change { config.exemptions.size }.by(-1)
|
63
|
+
|
64
|
+
expect(config.global?).to be(false)
|
65
|
+
expect(config.trusted_filters).to be_empty
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|