Prototype pollution vulnerability leading to arbitrary code execution in synchrony deobfuscator
Description
A prototype pollution vulnerability in synchrony deobfuscator before v2.4.4 allows remote code execution via crafted input.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A prototype pollution vulnerability in synchrony deobfuscator before v2.4.4 allows remote code execution via crafted input.
Vulnerability
Overview A __proto__ pollution vulnerability exists in synchrony deobfuscator versions before v2.4.4, specifically within the LiteralMap transformer. This flaw allows an attacker to modify properties of the Object prototype through crafted input [1][4].
Exploitation
Method An attacker can exploit this by providing a malicious input file that sets __proto__.parser to a path containing a JavaScript module. When synchrony processes this file, it triggers a require() call via the prettier module, leading to arbitrary code execution [4].
Impact
Successful exploitation enables an attacker to execute arbitrary code on the system running synchrony, potentially compromising the host environment [1][4].
Mitigation
The vulnerability is fixed in version v2.4.4 [2]. Users unable to upgrade should run Node.js with the --disable-proto=delete or --disable-proto=throw flags [1][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
deobfuscatornpm | >= 2.0.1, < 2.4.4 | 2.4.4 |
Affected products
2- relative/synchronyv5Range: >= 2.0.1, < 2.4.4
Patches
1b583126be94cMerge pull request from GHSA-jg82-xh3w-rhxx
7 files changed · +72 −112
package.json+0 −2 modified@@ -31,7 +31,6 @@ "@types/estree": "0.0.51", "@types/mersenne-twister": "1.1.2", "@types/node": "17.0.17", - "@types/prettier": "2.4.4", "@types/yargs": "17.0.8", "esbuild": "0.14.21", "escodegen": "2.0.0", @@ -47,7 +46,6 @@ "acorn-walk": "8.2.0", "eslint-scope": "7.1.1", "mersenne-twister": "1.1.0", - "prettier": "2.5.1", "yargs": "17.3.1" } }
pnpm-lock.yaml+4 −16 modified@@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@javascript-obfuscator/escodegen': specifier: 2.3.0 @@ -19,9 +23,6 @@ dependencies: mersenne-twister: specifier: 1.1.0 version: 1.1.0 - prettier: - specifier: 2.5.1 - version: 2.5.1 yargs: specifier: 17.3.1 version: 17.3.1 @@ -42,9 +43,6 @@ devDependencies: '@types/node': specifier: 17.0.17 version: 17.0.17 - '@types/prettier': - specifier: 2.4.4 - version: 2.4.4 '@types/yargs': specifier: 17.0.8 version: 17.0.8 @@ -122,10 +120,6 @@ packages: resolution: {integrity: sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw==} dev: true - /@types/prettier@2.4.4: - resolution: {integrity: sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA==} - dev: true - /@types/yargs-parser@20.2.1: resolution: {integrity: sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==} dev: true @@ -952,12 +946,6 @@ packages: resolution: {integrity: sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=} engines: {node: '>= 0.8.0'} - /prettier@2.5.1: - resolution: {integrity: sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==} - engines: {node: '>=10.13.0'} - hasBin: true - dev: false - /process@0.11.10: resolution: {integrity: sha1-czIwDoQBYb2j5podHZGn1LwW8YI=} engines: {node: '>= 0.6.0'}
src/context.ts+3 −5 modified@@ -69,7 +69,7 @@ interface ControlFlowLiteral { identifier: string value: string | number } -interface ControlFlowStorage { +export interface ControlFlowStorage { identifier: string aliases: string[] functions: ControlFlowFunction[] @@ -96,9 +96,7 @@ export default class Context { stringDecoders: DecoderFunction[] = [] stringDecoderReferences: DecoderReference[] = [] - controlFlowStorageNodes: { - [x: BlockId]: ControlFlowStorage - } = {} + controlFlowStorageNodes = new Map<BlockId, ControlFlowStorage>() removeGarbage: boolean = true transformers: InstanceType<typeof Transformer>[] @@ -112,7 +110,7 @@ export default class Context { ast: Program, transformers: [string, Partial<TransformerOptions>][], isModule: boolean, - source?: string, + source?: string ) { this.ast = ast this.transformers = this.buildTransformerList(transformers)
src/deobfuscator.ts+2 −36 modified@@ -4,7 +4,6 @@ import * as acornLoose from 'acorn-loose' import { Transformer, TransformerOptions } from './transformers/transformer' import { Node, Program, sp } from './util/types' import Context from './context' -import prettier from 'prettier' import { walk } from './util/walk' const FILE_REGEX = /(?<!\.d)\.[mc]?[jt]s$/i // cjs, mjs, js, ts, but no .d.ts @@ -44,6 +43,8 @@ export interface DeobfuscateOptions { * for work with Prettier * https://github.com/prettier/prettier/pull/12172 * (default = true) + * + * @deprecated Prettier is no longer used in the deobfuscator */ transformChainExpressions: boolean @@ -205,41 +206,6 @@ export class Deobfuscator { source = escodegen.generate(ast, { sourceMapWithCode: true, }).code - try { - source = prettier.format(source, { - semi: false, - singleQuote: true, - - // https://github.com/prettier/prettier/pull/12172 - parser: (text, _opts) => { - let ast = this.parse(text, acornOptions, options) - if (options.transformChainExpressions) { - walk(ast as Node, { - ChainExpression(cx) { - if (cx.expression.type === 'CallExpression') { - sp<any>(cx, { - ...cx.expression, - type: 'OptionalCallExpression', - expression: undefined, - }) - } else if (cx.expression.type === 'MemberExpression') { - sp<any>(cx, { - ...cx.expression, - type: 'OptionalMemberExpression', - expression: undefined, - }) - } - }, - }) - } - return ast - }, - }) - } catch (err) { - // I don't think we should log here, but throwing the error is not very - // important since it is non fatal - console.log(err) - } return source }
src/transformers/controlflow.ts+27 −19 modified@@ -10,11 +10,12 @@ import { Identifier, ObjectExpression, Statement, + BlockStatement, } from '../util/types' import { Transformer, TransformerOptions } from './transformer' import { walk } from '../util/walk' import * as Guard from '../util/guard' -import Context from '../context' +import Context, { ControlFlowStorage } from '../context' import { immutate, literalOrIdentifierToString, @@ -38,24 +39,33 @@ export default class ControlFlow extends Transformer<ControlFlowOptions> { if (!fx.body.body[0].argument) throw new TypeError('Function in CFSN was invalid (void return)') - let params = fx.params as Identifier[], - paramMap: { [ident: string]: Node } = {} + const params = fx.params as Identifier[], + paramMap = new Map<string, Node>() let i = 0 for (const p of params) { - paramMap[p.name] = cx.arguments[i] + paramMap.set(p.name, cx.arguments[i]) ++i } let immRtn = immutate(fx.body.body[0].argument) walk(immRtn, { Identifier(id) { - if (!paramMap[id.name]) return - sp<Node>(id, paramMap[id.name]) + const node = paramMap.get(id.name) + if (!node) return + sp<Node>(id, node) }, }) return immRtn as Node } + private getStorageNode( + context: Context, + node: BlockStatement + ): ControlFlowStorage | undefined { + const bid = getBlockId(node) + return context.controlFlowStorageNodes.get(bid) + } + // fixes empty object inits where there are setters in the same block populateEmptyObjects(context: Context) { walk(context.ast, { @@ -125,7 +135,8 @@ export default class ControlFlow extends Transformer<ControlFlowOptions> { // /shrug let bid = getBlockId(node) - if (context.controlFlowStorageNodes[bid]) return + let cfsn = context.controlFlowStorageNodes.get(bid) + if (cfsn) return if (node.body.length === 0) return walk(node, { @@ -145,13 +156,14 @@ export default class ControlFlow extends Transformer<ControlFlowOptions> { ) ) continue - context.controlFlowStorageNodes[bid] = { + + cfsn = { identifier: decl.id.name, aliases: [decl.id.name], functions: [], literals: [], } - const cfsn = context.controlFlowStorageNodes[bid] + context.controlFlowStorageNodes.set(bid, cfsn) for (const prop of decl.init.properties as PropertyLiteral[]) { let kn: Identifier | Literal = prop.key let key = ( @@ -228,12 +240,10 @@ export default class ControlFlow extends Transformer<ControlFlowOptions> { findStorageNodeAliases = (context: Context, ast: Node) => { walk(ast, { - BlockStatement(node) { - let bid = getBlockId(node) - - if (!context.controlFlowStorageNodes[bid]) return + BlockStatement: (node) => { if (node.body.length === 0) return - const cfsn = context.controlFlowStorageNodes[bid] + const cfsn = this.getStorageNode(context, node) + if (!cfsn) return walk(node, { VariableDeclaration(vd) { @@ -268,11 +278,9 @@ export default class ControlFlow extends Transformer<ControlFlowOptions> { replacer = (context: Context, ast: Node) => { const { translateCallExp } = this walk(ast, { - BlockStatement(node) { - const bid = getBlockId(node) - if (!context.controlFlowStorageNodes[bid]) return - const cfsn = context.controlFlowStorageNodes[bid] - + BlockStatement: (node) => { + const cfsn = this.getStorageNode(context, node) + if (!cfsn) return walk(node, { MemberExpression(mx) { if (!Guard.isIdentifier(mx.object)) return
src/transformers/jsconfuser/controlflow.ts+30 −27 modified@@ -48,9 +48,7 @@ function inverseOperator(operator: BinaryOperator) { throw new Error("Invalid operator to inverse '" + operator + "'") } } -interface VarStack { - [x: string]: number -} +type VarStack = Map<string, number> function generateCode(ast: Node): string { return escodegen.generate(ast as any, { sourceMapWithCode: true, @@ -69,41 +67,46 @@ function evaluateAssignmentExpr( operator: AssignmentOperator, value: number ) { + if (operator === '=') return stack.set(vk, value) + + const stackVal = stack.get(vk) + if (typeof stackVal !== 'number') + throw new Error( + 'Unexpected non-numeric value in jsconfuser controlflow stack' + ) + switch (operator) { - case '=': - return (stack[vk] = value) case '+=': - return (stack[vk] += value) + return stack.set(vk, stackVal + value) case '-=': - return (stack[vk] -= value) + return stack.set(vk, stackVal - value) case '*=': - return (stack[vk] *= value) + return stack.set(vk, stackVal * value) case '/=': - return (stack[vk] /= value) + return stack.set(vk, stackVal / value) case '%=': - return (stack[vk] %= value) + return stack.set(vk, stackVal % value) case '<<=': - return (stack[vk] <<= value) + return stack.set(vk, stackVal << value) case '>>=': - return (stack[vk] >>= value) + return stack.set(vk, stackVal >> value) case '>>>=': - return (stack[vk] >>>= value) + return stack.set(vk, stackVal >>> value) case '&=': - return (stack[vk] &= value) + return stack.set(vk, stackVal & value) case '^=': - return (stack[vk] ^= value) + return stack.set(vk, stackVal ^ value) case '|=': - return (stack[vk] |= value) + return stack.set(vk, stackVal | value) default: throw new Error( 'Invalid assignment expression operator "' + operator + '"' ) } } function updateIdentifiers(stack: VarStack, obj: any) { - for (const vk in stack) { - let value = stack[vk], - node = createLiteral(value) + for (const [vk, value] of stack) { + const node = createLiteral(value) walk(obj, { Identifier(id) { @@ -149,7 +152,7 @@ function evaluateSequenceAssignments( continue } if (!Guard.isIdentifier(expr.left)) continue - if (!(expr.left.name in stack)) continue + if (!stack.has(expr.left.name)) continue const vk = expr.left.name, operator = expr.operator @@ -179,7 +182,7 @@ function evaluateSequenceAssignments( let effect = literalOrUnaryExpressionToNumber(ie) evaluateAssignmentExpr(stack, vk, operator, effect) - log(`stack[${vk}] = ${stack[vk]}`) + log(`stack[${vk}] = ${stack.get(vk)}`) log('='.repeat(32)) ;(expr as any).type = 'EmptyStatement' } @@ -208,24 +211,24 @@ export default class JSCControlFlow extends Transformer<JSCControlFlowOptions> { ) continue - const stack: VarStack = {} + const stack: VarStack = new Map() let bx = w.test, additive = false while (Guard.isBinaryExpression(bx)) { additive = bx.operator === '+' if (Guard.isIdentifier(bx.left)) { - stack[bx.left.name] = bx.left.start + stack.set(bx.left.name, bx.left.start) } if (Guard.isIdentifier(bx.right)) { - stack[bx.right.name] = bx.right.start + stack.set(bx.right.name, bx.right.start) } bx = bx.left as BinaryExpression } if (!additive) continue - for (const vk in stack) { + for (const [vk, value] of stack) { let vref = scope.references.find( - (i) => i.identifier.range![0] === stack[vk] + (i) => i.identifier.range![0] === value ) if (!vref) continue if ( @@ -246,7 +249,7 @@ export default class JSCControlFlow extends Transformer<JSCControlFlowOptions> { i.range![0] !== def.node.range![0] && i.range![1] !== def.node.range![1] ) - stack[vk] = literalOrUnaryExpressionToNumber(def.node.init) + stack.set(vk, literalOrUnaryExpressionToNumber(def.node.init)) } const endState = literalOrUnaryExpressionToNumber(w.test.right) context.log(stack, endState)
src/transformers/literalmap.ts+6 −7 modified@@ -24,7 +24,7 @@ export default class LiteralMap extends Transformer<LiteralMapOptions> { demap(context: Context) { walk(context.ast, { BlockStatement(node) { - const map: { [x: string]: { [x: string]: any } } = {} + const map = new Map<string, Map<string, any>>() walk(node, { VariableDeclaration(vd) { @@ -48,16 +48,16 @@ export default class LiteralMap extends Transformer<LiteralMapOptions> { continue const name = decl.id.name - map[name] = map[name] || {} - + const localMap = map.get(name) || new Map<string, any>() for (const _prop of decl.init.properties) { const prop = _prop as Property let key = prop.key.type === 'Identifier' ? prop.key.name : ((prop.key as Literal).value as string) - map[name][key] = (prop.value as Literal).value as string + localMap.set(key, (prop.value as Literal).value as string) } + if (!map.has(name)) map.set(name, localMap) if (context.removeGarbage) { rm.push(`${decl.start}!${decl.end}`) @@ -77,13 +77,13 @@ export default class LiteralMap extends Transformer<LiteralMapOptions> { !Guard.isIdentifier(exp.property)) ) return - let mapObj = map[exp.object.name] + let mapObj = map.get(exp.object.name) if (!mapObj) return let key = Guard.isIdentifier(exp.property) ? exp.property.name : ((exp.property as Literal).value as string) - let val = mapObj[key] + let val = mapObj.get(key) if (typeof val === 'undefined') return // ! check causes !0 == true. sp<Literal>(exp, { type: 'Literal', @@ -104,7 +104,6 @@ export default class LiteralMap extends Transformer<LiteralMapOptions> { if (!scope) return for (const v of scope.variables) { - if (/*func.start === 3547 && */ v.name === 'q') debugger if (v.name === 'arguments') continue if (v.identifiers.length !== 1) continue // ? if (v.defs.length !== 1) continue // ?
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-jg82-xh3w-rhxxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-45811ghsaADVISORY
- github.com/relative/synchrony/commit/b583126be94c4db7c5a478f1c5204bfb4162cf40ghsax_refsource_MISCWEB
- github.com/relative/synchrony/security/advisories/GHSA-jg82-xh3w-rhxxghsax_refsource_CONFIRMWEB
- github.com/relative/synchrony/security/advisories/src/transformers/literalmap.tsghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.