From 598f6a0ffa11f8739c41552d9f3c5ee64e7e27a4 Mon Sep 17 00:00:00 2001 From: Sebastion Date: Mon, 11 May 2026 22:45:48 +0100 Subject: [PATCH] fix(ExecuteStage): prevent CWE-95 code injection in Field[...] templates Field["x"] tokens substituted into the body of an e v a l("...") or JS("...") template were inserted as raw source. Because outputParameters values come from scraped page content, any attacker controlling a crawled page could break out of the surrounding string literal and execute arbitrary Python on the user's machine (or arbitrary JS in the browser context). Substitute Field["x"] inside Python expression bodies via repr() and inside JS() bodies via json.dumps() so the scraped value is always a well-formed, properly-escaped string literal that cannot escape its quoting. Field expansions outside of those calls are still inserted as plain text. --- ExecuteStage/utils.py | 100 +++++++++++++++++++++++++++++++----------- 1 file changed, 74 insertions(+), 26 deletions(-) diff --git a/ExecuteStage/utils.py b/ExecuteStage/utils.py index f830f0f..481fbee 100644 --- a/ExecuteStage/utils.py +++ b/ExecuteStage/utils.py @@ -315,35 +315,83 @@ def write_to_csv(file_name, data, record): f_csv.writerow(to_write) f.close() +def _substitute_fields_as_python_literal(text, outputParameters): + """Replace Field["x"] tokens inside a Python expression with safe repr() literals. + + Scraped values are user-controlled and must NEVER be interpolated as raw + source code into an eval() expression — that is arbitrary code execution + (CWE-95). Wrapping the value with repr() turns the substitution into a + well-formed Python string literal that cannot break out of its quoting. + """ + field_pat = re.compile(r'Field\["([^"]+)"\]') + return field_pat.sub( + lambda m: repr(str(outputParameters.get(m.group(1), ''))), + text, + ) + + +def _substitute_fields_as_js_literal(text, outputParameters): + """Replace Field["x"] tokens inside a JS snippet with safe json.dumps() literals. + + The JS("...") feature executes the resulting snippet inside the browser. If + scraped Field values are pasted directly into the snippet they can break + out of the surrounding string literal and inject arbitrary JS (CWE-95 in + the page/extension context). json.dumps produces a JSON string literal + that is also a valid, safely-escaped JS string literal. + """ + field_pat = re.compile(r'Field\["([^"]+)"\]') + return field_pat.sub( + lambda m: json.dumps(str(outputParameters.get(m.group(1), ''))), + text, + ) + + def replace_field_values(orginal_text, outputParameters, browser=None): - pattern = r'Field\["([^"]+)"\]' + """Expand Field["x"] / eval("...") / JS("...") templates safely. + + Field["x"] expansions outside of eval()/JS() calls are inserted as plain + text. Field["x"] expansions *inside* eval()/JS() calls are inserted as + properly escaped Python/JS string literals so that scraped page content + cannot escape the surrounding quotes and execute arbitrary code. + """ + field_pattern = r'Field\["([^"]+)"\]' try: + # Operate on the ORIGINAL template first so we can tell which Field + # references live inside eval()/JS() calls (and therefore need to be + # escaped as string literals) versus those that are plain text. + working_text = orginal_text + if re.search(r'eval\(', working_text, re.IGNORECASE): + working_text = working_text.replace("self.", "browser.") + eval_call_pattern = re.compile(r'(?i)eval\("(.+?)"\)') + while True: + match = eval_call_pattern.search(working_text) + if not match: + break + # Substitute Field["x"] inside the eval body using repr() so + # that scraped content becomes a safe Python string literal. + expr = _substitute_fields_as_python_literal( + match.group(1), outputParameters) + eval_replaced_text = str(eval(expr)) # noqa: S307 - documented feature, scraped inputs are escaped above + working_text = working_text.replace( + match.group(0), eval_replaced_text) + if re.search(r'JS\(', working_text, re.IGNORECASE): + working_text = working_text.replace("self.", "browser.") + js_call_pattern = re.compile(r'(?i)JS\("(.+?)"\)') + while True: + match = js_call_pattern.search(working_text) + if not match: + break + snippet = _substitute_fields_as_js_literal( + match.group(1), outputParameters) + JS_replaced_text = str(browser.browser.execute_script(snippet)) + working_text = working_text.replace( + match.group(0), JS_replaced_text) + # Finally expand any remaining Field["x"] tokens as plain text. replaced_text = re.sub( - pattern, lambda match: outputParameters.get(match.group(1), ''), orginal_text) - if re.search(r'eval\(', replaced_text, re.IGNORECASE): # 如果返回值中包含EVAL - replaced_text = replaced_text.replace("self.", "browser.") - pattern = re.compile(r'(?i)eval\("(.+?)"\)') - # 循环替换所有匹配到的eval语句 - while True: - match = pattern.search(replaced_text) - if not match: - break - # 执行eval并将其结果转换为字符串形式 - eval_replaced_text = str(eval(match.group(1))) - # 替换eval语句 - replaced_text = replaced_text.replace(match.group(0), eval_replaced_text) - if re.search(r'JS\(', replaced_text, re.IGNORECASE): # 如果返回值中包含JS - replaced_text = replaced_text.replace("self.", "browser.") - pattern = re.compile(r'(?i)JS\("(.+?)"\)') - # 循环替换所有匹配到的JS语句 - while True: - match = pattern.search(replaced_text) - if not match: - break - # 执行JS并将其结果转换为字符串形式 - JS_replaced_text = str(browser.browser.execute_script(match.group(1))) - # 替换JS语句 - replaced_text = replaced_text.replace(match.group(0), JS_replaced_text) + field_pattern, + lambda match: str(outputParameters.get(match.group(1), '')), + working_text, + ) except Exception as e: print("eval替换失败,请检查eval语句是否正确。| Failed to replace eval, please check if the eval statement is correct.") print(e)