From b3cf7ce6ba51e6d35b55d466d5fcc0a3367b6976 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 20 Dec 2024 00:35:11 -0700 Subject: [PATCH] fix(toolbox/getcontext): adding resiliency to getContext to prevent keyword usage errors --- .../src/getContext/getContext.test.ts | 159 +++++++++++++++++- .../snap-toolbox/src/getContext/getContext.ts | 82 ++++++++- 2 files changed, 229 insertions(+), 12 deletions(-) diff --git a/packages/snap-toolbox/src/getContext/getContext.test.ts b/packages/snap-toolbox/src/getContext/getContext.test.ts index 6c76d01ff..0cbf4b818 100644 --- a/packages/snap-toolbox/src/getContext/getContext.test.ts +++ b/packages/snap-toolbox/src/getContext/getContext.test.ts @@ -260,16 +260,18 @@ describe('getContext', () => { }); }); - it('supports evaluation of all valid javascript', () => { + it('supports evaluation of all valid javascript - errors will not throw but will be undefined', () => { const scriptTag = document.createElement('script'); scriptTag.setAttribute('type', 'searchspring/recommend'); scriptTag.innerHTML = ` error = window.dne.property `; + let vars: { [key: string]: any } = {}; expect(() => { - getContext(['error'], scriptTag); - }).toThrow(); + vars = getContext(['error'], scriptTag); + }).not.toThrow(); + expect(vars?.error).toBeUndefined(); }); it('does not throw an error when variables exist already, but are not in evaluation list', () => { @@ -285,4 +287,155 @@ describe('getContext', () => { getContext(['error'], scriptTag); }).not.toThrow(); }); + + it('does not attempt to evaluate variable assignments when they are within quotes', () => { + const scriptTag = document.createElement('script'); + scriptTag.setAttribute('type', 'searchspring/recommend'); + scriptTag.setAttribute('id', 'searchspring-recommend'); + scriptTag.innerHTML = 'format = "${{amount}}";'; + + expect(() => { + const vars = getContext(['format'], scriptTag); + + expect(vars).toHaveProperty('format', '${{amount}}'); + }).not.toThrow(); + }); + + it('throws an error when JavaScript keywords are provided in the evaluate array', () => { + const scriptTag = document.createElement('script'); + scriptTag.setAttribute('type', 'searchspring'); + + // invalid param that should throw + expect(() => { + getContext(['class'], scriptTag); + }).toThrow('getContext: JavaScript keywords are not allowed in evaluate array'); + + expect(() => { + getContext(['const'], scriptTag); + }).toThrow('getContext: JavaScript keywords are not allowed in evaluate array'); + + expect(() => { + getContext(['if'], scriptTag); + }).toThrow('getContext: JavaScript keywords are not allowed in evaluate array'); + }); + + it('throws an error when JavaScript keywords are found in script inner variables', () => { + const scriptTag = document.createElement('script'); + scriptTag.setAttribute('type', 'searchspring'); + scriptTag.innerHTML = ` + class = "should-not-evaluate"; + const = "should-not-evaluate"; + if = "should-not-evaluate"; + validVar = "should-evaluate"; + `; + + expect(() => { + getContext(['validVar'], scriptTag); + }).toThrow('getContext: JavaScript keywords cannot be used as variable names in script'); + }); +}); + +describe('variable name parsing', () => { + it('correctly identifies variable names when quotes are present', () => { + const scriptTag = document.createElement('script'); + scriptTag.setAttribute('type', 'searchspring'); + scriptTag.innerHTML = ` + realVar = "something = 123"; + anotherVar = 'test = value'; + actualValue = 456; + `; + + const vars = getContext(['realVar', 'anotherVar', 'actualValue', 'something', 'test'], scriptTag); + expect(Object.keys(vars)).toHaveLength(3); + expect(vars).toHaveProperty('realVar', 'something = 123'); + expect(vars).toHaveProperty('anotherVar', 'test = value'); + expect(vars).toHaveProperty('actualValue', 456); + expect(vars).not.toHaveProperty('something'); + expect(vars).not.toHaveProperty('test'); + }); + + it('handles template literals correctly', () => { + const scriptTag = document.createElement('script'); + scriptTag.setAttribute('type', 'searchspring'); + scriptTag.innerHTML = ` + template = \` +
+ \${value} + \${name} +
+ \`; + actual = "real value"; + `; + + const vars = getContext(['template', 'actual', 'value', 'name'], scriptTag); + expect(Object.keys(vars)).toHaveLength(2); + expect(vars).toHaveProperty('template'); + expect(vars).toHaveProperty('actual', 'real value'); + }); + + it('handles HTML attributes that look like assignments', () => { + const scriptTag = document.createElement('script'); + scriptTag.setAttribute('type', 'searchspring'); + scriptTag.innerHTML = ` + html = '
'; + value = 'real value'; + `; + + const vars = getContext(['html', 'value'], scriptTag); + expect(Object.keys(vars)).toHaveLength(2); + expect(vars).toHaveProperty('html'); + expect(vars).toHaveProperty('value', 'real value'); + }); + + it('handles nested quotes correctly', () => { + const scriptTag = document.createElement('script'); + scriptTag.setAttribute('type', 'searchspring'); + scriptTag.innerHTML = ` + config = "{ \\"nested = value\\": true }"; + actual = 123; + `; + + const vars = getContext(['config', 'actual', 'nested'], scriptTag); + expect(Object.keys(vars)).toHaveLength(2); + expect(vars).toHaveProperty('config'); + expect(vars).toHaveProperty('actual', 123); + expect(vars).not.toHaveProperty('nested'); + }); +}); + +describe('javascript keywords', () => { + it('filters out javascript keywords from evaluation', () => { + const scriptTag = document.createElement('script'); + scriptTag.setAttribute('type', 'searchspring'); + scriptTag.innerHTML = ` + class = "should-not-evaluate"; + const = "should-not-evaluate"; + if = "should-not-evaluate"; + validVar = "should-evaluate"; + `; + + expect(() => { + const vars = getContext(['class', 'const', 'if', 'validVar'], scriptTag); + }).toThrow('getContext: JavaScript keywords are not allowed in evaluate array'); + }); + + it('allows javascript keywords in object properties and string values', () => { + const scriptTag = document.createElement('script'); + scriptTag.setAttribute('type', 'searchspring'); + scriptTag.innerHTML = ` + config = { + class: "my-class", + const: "my-const", + if: true + }; + `; + + const vars = getContext(['config'], scriptTag); + expect(vars).toHaveProperty('config'); + expect(vars.config).toEqual({ + class: 'my-class', + const: 'my-const', + if: true, + }); + }); }); diff --git a/packages/snap-toolbox/src/getContext/getContext.ts b/packages/snap-toolbox/src/getContext/getContext.ts index f55304f90..a6eba1c1a 100644 --- a/packages/snap-toolbox/src/getContext/getContext.ts +++ b/packages/snap-toolbox/src/getContext/getContext.ts @@ -2,7 +2,57 @@ type ContextVariables = { [variable: string]: any; }; +const JAVASCRIPT_KEYWORDS = new Set([ + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'export', + 'extends', + 'finally', + 'for', + 'function', + 'if', + 'import', + 'in', + 'instanceof', + 'new', + 'return', + 'super', + 'switch', + 'this', + 'throw', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', + 'let', + 'static', + 'enum', + 'await', + 'implements', + 'package', + 'protected', + 'interface', + 'private', + 'public', +]); + export function getContext(evaluate: string[] = [], script?: HTMLScriptElement | string): ContextVariables { + if (evaluate?.some((name) => JAVASCRIPT_KEYWORDS.has(name))) { + throw new Error('getContext: JavaScript keywords are not allowed in evaluate array'); + } + if (!script || typeof script === 'string') { const scripts = Array.from(document.querySelectorAll((script as string) || 'script[id^=searchspring], script[src*="snapui.searchspring.io"]')); @@ -49,24 +99,38 @@ export function getContext(evaluate: string[] = [], script?: HTMLScriptElement | const scriptInnerHTML = scriptElem.innerHTML; // attempt to grab inner HTML variables - const scriptInnerVars = scriptInnerHTML.match(/([a-zA-Z_$][a-zA-Z_$0-9]*)\s?=/g)?.map((match) => match.replace(/[\s=]/g, '')); + const scriptInnerVars = scriptInnerHTML + // first remove all string literals (including template literals) to avoid false matches + .replace(/`(?:\\[\s\S]|[^`\\])*`|'(?:\\[\s\S]|[^'\\])*'|"(?:\\[\s\S]|[^"\\])*"/g, '') + // then find variable assignments + .match(/([a-zA-Z_$][a-zA-Z_$0-9]*)\s*=/g) + ?.map((match) => match.replace(/[\s=]/g, '')); + + if (scriptInnerVars?.some((name) => JAVASCRIPT_KEYWORDS.has(name))) { + throw new Error('getContext: JavaScript keywords cannot be used as variable names in script'); + } const combinedVars = evaluate.concat(scriptInnerVars || []); // de-dupe vars const evaluateVars = combinedVars.filter((item, index) => { - return combinedVars.indexOf(item) === index; + return combinedVars.indexOf(item) === index && !JAVASCRIPT_KEYWORDS.has(item); }); // evaluate text and put into variables evaluate?.forEach((name) => { - const fn = new Function(` - var ${evaluateVars.join(', ')}; - ${scriptInnerHTML} - return ${name}; - `); - - scriptVariables[name] = fn(); + try { + const fn = new Function(` + var ${evaluateVars.join(', ')}; + ${scriptInnerHTML} + return ${name}; + `); + scriptVariables[name] = fn(); + } catch (e) { + // if evaluation fails, set to undefined + console.log(`getContext: error evaluating ${name}`); + scriptVariables[name] = undefined; + } }); const variables = {