gtl.py 1 """Gruyere Template Language, part of Gruyere, a web application with holes. 2 3 Copyright 2017 Google Inc. All rights reserved. 4 5 This code is licensed under the https://creativecommons.org/licenses/by-nd/3.0/us/ 6 Creative Commons Attribution-No Derivative Works 3.0 United States license. 7 8 DO NOT COPY THIS CODE! 9 10 This application is a small self-contained web application with numerous 11 security holes. It is provided for use with the Web Application Exploits and 12 Defenses codelab. You may modify the code for your own use while doing the 13 codelab but you may not distribute the modified code. Brief excerpts of this 14 code may be used for educational or instructional purposes provided this 15 notice is kept intact. By using Gruyere you agree to the Terms of Service 16 https://www.google.com/intl/en/policies/terms/ 17 """ 18 19 __author__ = 'Bruce Leban' 20 21 # system modules 22 import cgi 23 import logging 24 import operator 25 import os 26 import pprint 27 import sys 28 29 # our modules 30 import gruyere 31 import sanitize 32 33 34 def ExpandTemplate(template, specials, params, name=''): 35 """Expands a template. 36 37 Args: 38 template: a string template. 39 specials: a dict of special values. 40 params: a dict of parameter values. 41 name: the name of the _this object. 42 43 Returns: 44 the expanded template. 45 46 The template language includes these block structures: 47 48 [[include:<filename>]] ...[[/include:<filename>]] 49 Insert the file or if the file cannot be opened insert the contents of 50 the block. The path should use / as a separator regardless of what 51 the underlying operating system is. 52 53 [[for:<variable>]] ... [[/for:<variable>]] 54 Iterate over the variable (which should be a mapping or sequence) and 55 insert the block once for each value. Inside the loop _key is bound to 56 the key value for the iteration. 57 58 [[if:<variable>]] ... [[/if:<variable>]] 59 Expand the contents of the block if the variable is not 'false'. There 60 is no else; use [[if:!<variable>]] instead. 61 62 Note that in each case the end tags must match the begin tags with a 63 leading slash. This prevents mismatched tags and makes it easier to parse. 64 65 The variable syntax is: 66 67 {{<field>[.<field>]*[:<escaper>]}} 68 69 where <field> is: 70 71 a key to extract from a mapping 72 a number to extract from a sequence 73 74 Variable names that start with '_' are special values: 75 _key = iteration key (inside loops) 76 _this = iteration value (inside loop) 77 _db = the database 78 _cookie = the user's cookie 79 _profile = the user's profile ~ _db.*(_cookie.user) 80 81 If a field name starts with '*' it refers to a dereferenced parameter (orx 82 *_this). For example, _db.*uid retrieves the entry from _db matching the 83 uid parameter. 84 85 The comment syntax is: 86 87 {{#<comment>}} 88 """ 89 t = _ExpandBlocks(template, specials, params, name) 90 t = _ExpandVariables(t, specials, params, name) 91 return t 92 93 94 BLOCK_OPEN = '[[' 95 END_BLOCK_OPEN = '[[/' 96 BLOCK_CLOSE = ']]' 97 98 99 def _ExpandBlocks(template, specials, params, name): 100 """Expands all the blocks in a template.""" 101 result = [] 102 rest = template 103 while rest: 104 tag, before_tag, after_tag = _FindTag(rest, BLOCK_OPEN, BLOCK_CLOSE) 105 if tag is None: 106 break 107 end_tag = END_BLOCK_OPEN + tag + BLOCK_CLOSE 108 before_end = rest.find(end_tag, after_tag) 109 if before_end < 0: 110 break 111 after_end = before_end + len(end_tag) 112 113 result.append(rest[:before_tag]) 114 block = rest[after_tag:before_end] 115 result.append(_ExpandBlock(tag, block, specials, params, name)) 116 rest = rest[after_end:] 117 return ''.join(result) + rest 118 119 120 VAR_OPEN = '{{' 121 VAR_CLOSE = '}}' 122 123 124 def _ExpandVariables(template, specials, params, name): 125 """Expands all the variables in a template.""" 126 result = [] 127 rest = template 128 while rest: 129 tag, before_tag, after_tag = _FindTag(rest, VAR_OPEN, VAR_CLOSE) 130 if tag is None: 131 break 132 result.append(rest[:before_tag]) 133 result.append(str(_ExpandVariable(tag, specials, params, name))) 134 rest = rest[after_tag:] 135 return ''.join(result) + rest 136 137 138 FOR_TAG = 'for' 139 IF_TAG = 'if' 140 INCLUDE_TAG = 'include' 141 142 143 def _ExpandBlock(tag, template, specials, params, name): 144 """Expands a single template block.""" 145 146 tag_type, block_var = tag.split(':', 1) 147 if tag_type == INCLUDE_TAG: 148 return _ExpandInclude(tag, block_var, template, specials, params, name) 149 elif tag_type == IF_TAG: 150 block_data = _ExpandVariable(block_var, specials, params, name) 151 if block_data: 152 return ExpandTemplate(template, specials, params, name) 153 return '' 154 elif tag_type == FOR_TAG: 155 block_data = _ExpandVariable(block_var, specials, params, name) 156 return _ExpandFor(tag, template, specials, block_data) 157 else: 158 _Log('Error: Invalid block: %s' % (tag,)) 159 return '' 160 161 162 def _ExpandInclude(_, filename, template, specials, params, name): 163 """Expands an include block (or insert the template on an error).""" 164 result = '' 165 # replace /s with local file system equivalent 166 fname = os.sep + filename.replace('/', os.sep) 167 f = None 168 try: 169 try: 170 f = gruyere._Open(gruyere.RESOURCE_PATH, fname) 171 result = f.read() 172 except IOError: 173 _Log('Error: missing filename: %s' % (filename,)) 174 result = template 175 finally: 176 if f: f.close() 177 return ExpandTemplate(result, specials, params, name) 178 179 180 def _ExpandFor(tag, template, specials, block_data): 181 """Expands a for block iterating over the block_data.""" 182 result = [] 183 if operator.isMappingType(block_data): 184 for v in block_data: 185 result.append(ExpandTemplate(template, specials, block_data[v], v)) 186 elif operator.isSequenceType(block_data): 187 for i in xrange(len(block_data)): 188 result.append(ExpandTemplate(template, specials, block_data[i], str(i))) 189 else: 190 _Log('Error: Invalid type: %s' % (tag,)) 191 return '' 192 return ''.join(result) 193 194 195 def _ExpandVariable(var, specials, params, name, default=''): 196 """Gets a variable value.""" 197 if var.startswith('#'): # this is a comment. 198 return '' 199 200 # Strip out leading ! which negates value 201 inverted = var.startswith('!') 202 if inverted: 203 var = var[1:] 204 205 # Strip out trailing :<escaper> 206 escaper_name = None 207 if var.find(':') >= 0: 208 (var, escaper_name) = var.split(':', 1) 209 210 value = _ExpandValue(var, specials, params, name, default) 211 if inverted: 212 value = not value 213 214 if escaper_name == 'text': 215 value = cgi.escape(str(value)) 216 elif escaper_name == 'html': 217 value = sanitize.SanitizeHtml(str(value)) 218 elif escaper_name == 'pprint': # for debugging 219 value = '<pre>' + cgi.escape(pprint.pformat(value)) + '</pre>' 220 221 if value is None: 222 value = '' 223 return value 224 225 226 def _ExpandValue(var, specials, params, name, default): 227 """Expand one value. 228 229 This expands the <field>.<field>...<field> part of the variable 230 expansion. A field may be of the form *<param> to use the value 231 of a parameter as the field name. 232 """ 233 if var == '_key': 234 return name 235 elif var == '_this': 236 return params 237 if var.startswith('_'): 238 value = specials 239 else: 240 value = params 241 242 for v in var.split('.'): 243 if v == '*_this': 244 v = params 245 if v.startswith('*'): 246 v = _GetValue(specials['_params'], v[1:]) 247 if operator.isSequenceType(v): 248 v = v[0] # reduce repeated url param to single value 249 value = _GetValue(value, str(v), default) 250 return value 251 252 253 def _GetValue(collection, index, default=''): 254 """Gets a single indexed value out of a collection. 255 256 The index is either a key in a mapping or a numeric index into 257 a sequence. 258 259 Returns: 260 value 261 """ 262 if operator.isMappingType(collection) and index in collection: 263 value = collection[index] 264 elif (operator.isSequenceType(collection) and index.isdigit() and 265 int(index) < len(collection)): 266 value = collection[int(index)] 267 else: 268 value = default 269 return value 270 271 272 def _Cond(test, if_true, if_false): 273 """Substitute for 'if_true if test else if_false' in Python 2.4.""" 274 if test: 275 return if_true 276 else: 277 return if_false 278 279 280 def _FindTag(template, open_marker, close_marker): 281 """Finds a single tag. 282 283 Args: 284 template: the template to search. 285 open_marker: the start of the tag (e.g., '{{'). 286 close_marker: the end of the tag (e.g., '}}'). 287 288 Returns: 289 (tag, pos1, pos2) where the tag has the open and close markers 290 stripped off and pos1 is the start of the tag and pos2 is the end of 291 the tag. Returns (None, None, None) if there is no tag found. 292 """ 293 open_pos = template.find(open_marker) 294 close_pos = template.find(close_marker, open_pos) 295 if open_pos < 0 or close_pos < 0 or open_pos > close_pos: 296 return (None, None, None) 297 return (template[open_pos + len(open_marker):close_pos], 298 open_pos, 299 close_pos + len(close_marker)) 300 301 302 def _Log(message): 303 logging.warning('%s', message) 304 print >>sys.stderr, message