Skip to content

Commit

Permalink
fix(toolbox/getcontext): adding resiliency to getContext to prevent k…
Browse files Browse the repository at this point in the history
…eyword usage errors
  • Loading branch information
korgon committed Dec 20, 2024
1 parent 46278e1 commit b3cf7ce
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 12 deletions.
159 changes: 156 additions & 3 deletions packages/snap-toolbox/src/getContext/getContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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 = "<span class=money>${{amount}}</span>";';

expect(() => {
const vars = getContext(['format'], scriptTag);

expect(vars).toHaveProperty('format', '<span class=money>${{amount}}</span>');
}).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 = \`
<div>
\${value}
\${name}
</div>
\`;
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 = '<div class="test" data-value="something = 123"></div>';
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,
});
});
});
82 changes: 73 additions & 9 deletions packages/snap-toolbox/src/getContext/getContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]'));

Expand Down Expand Up @@ -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 = {
Expand Down

0 comments on commit b3cf7ce

Please sign in to comment.