@@ -1500,6 +1500,137 @@ describe('JsTaskRunner', () => {
15001500 } ) ;
15011501 } ) ;
15021502
1503+ describe ( 'Error.prepareStackTrace RCE prevention' , ( ) => {
1504+ test . each ( [ [ 'runOnceForAllItems' ] , [ 'runOnceForEachItem' ] ] as const ) (
1505+ 'should block Error.prepareStackTrace access in %s mode' ,
1506+ async ( mode ) => {
1507+ const execute = mode === 'runOnceForAllItems' ? executeForAllItems : executeForEachItem ;
1508+ const outcome = await execute ( {
1509+ code :
1510+ mode === 'runOnceForAllItems'
1511+ ? 'return [{ json: { result: Error.prepareStackTrace } }]'
1512+ : 'return { json: { result: Error.prepareStackTrace } }' ,
1513+ inputItems : [ { a : 1 } ] ,
1514+ } ) ;
1515+ expect ( outcome . result [ 0 ] . json . result ) . toBeUndefined ( ) ;
1516+ } ,
1517+ ) ;
1518+
1519+ test . each ( [ [ 'runOnceForAllItems' ] , [ 'runOnceForEachItem' ] ] as const ) (
1520+ 'should block Error.captureStackTrace access in %s mode' ,
1521+ async ( mode ) => {
1522+ const execute = mode === 'runOnceForAllItems' ? executeForAllItems : executeForEachItem ;
1523+ const outcome = await execute ( {
1524+ code :
1525+ mode === 'runOnceForAllItems'
1526+ ? 'return [{ json: { result: Error.captureStackTrace } }]'
1527+ : 'return { json: { result: Error.captureStackTrace } }' ,
1528+ inputItems : [ { a : 1 } ] ,
1529+ } ) ;
1530+ expect ( outcome . result [ 0 ] . json . result ) . toBeUndefined ( ) ;
1531+ } ,
1532+ ) ;
1533+
1534+ test . each ( [ [ 'runOnceForAllItems' ] , [ 'runOnceForEachItem' ] ] as const ) (
1535+ 'should prevent Object.defineProperty from setting Error.prepareStackTrace in %s mode' ,
1536+ async ( mode ) => {
1537+ const execute = mode === 'runOnceForAllItems' ? executeForAllItems : executeForEachItem ;
1538+ // First try to define prepareStackTrace, then check if it's still undefined
1539+ const outcome = await execute ( {
1540+ code :
1541+ mode === 'runOnceForAllItems'
1542+ ? `
1543+ Object.defineProperty(Error, 'prepareStackTrace', { value: () => 'pwned' });
1544+ return [{ json: { result: Error.prepareStackTrace } }];
1545+ `
1546+ : `
1547+ Object.defineProperty(Error, 'prepareStackTrace', { value: () => 'pwned' });
1548+ return { json: { result: Error.prepareStackTrace } };
1549+ ` ,
1550+ inputItems : [ { a : 1 } ] ,
1551+ } ) ;
1552+ // prepareStackTrace should still be undefined because defineProperty is neutered
1553+ expect ( outcome . result [ 0 ] . json . result ) . toBeUndefined ( ) ;
1554+ } ,
1555+ ) ;
1556+
1557+ test ( 'should prevent full RCE exploit chain via prepareStackTrace' , async ( ) => {
1558+ // This is the actual exploit payload - should NOT execute system commands
1559+ // Even though the code runs without throwing, the exploit should not work
1560+ // because Object.defineProperty is neutered and prepareStackTrace remains undefined
1561+ const outcome = await executeForAllItems ( {
1562+ code : `
1563+ Object.defineProperty(Error, 'prepareStackTrace', {
1564+ value: (e, stack) => {
1565+ const g = stack[0].getThis();
1566+ return g.global.process.getBuiltinModule('child_process').execSync('echo pwned').toString();
1567+ }
1568+ });
1569+ const result = new Error().stack;
1570+ // Check if the result contains 'pwned' - if so, exploit succeeded
1571+ const exploited = typeof result === 'string' && result.includes('pwned');
1572+ return [{ json: { exploited, result: typeof result } }];
1573+ ` ,
1574+ inputItems : [ { a : 1 } ] ,
1575+ } ) ;
1576+ // The exploit should fail - prepareStackTrace wasn't actually set
1577+ // so .stack returns a normal stack trace string, not 'pwned'
1578+ expect ( outcome . result [ 0 ] . json . exploited ) . toBe ( false ) ;
1579+ expect ( outcome . result [ 0 ] . json . result ) . toBe ( 'string' ) ; // Normal stack trace
1580+ } ) ;
1581+ } ) ;
1582+
1583+ describe ( 'Object.defineProperty security' , ( ) => {
1584+ test . each ( [ [ 'runOnceForAllItems' ] , [ 'runOnceForEachItem' ] ] as const ) (
1585+ 'should neuter Object.defineProperty in %s mode' ,
1586+ async ( mode ) => {
1587+ const execute = mode === 'runOnceForAllItems' ? executeForAllItems : executeForEachItem ;
1588+ const outcome = await execute ( {
1589+ code :
1590+ mode === 'runOnceForAllItems'
1591+ ? `
1592+ const obj = {};
1593+ Object.defineProperty(obj, 'test', { value: 123 });
1594+ return [{ json: { hasProperty: 'test' in obj, value: obj.test } }];
1595+ `
1596+ : `
1597+ const obj = {};
1598+ Object.defineProperty(obj, 'test', { value: 123 });
1599+ return { json: { hasProperty: 'test' in obj, value: obj.test } };
1600+ ` ,
1601+ inputItems : [ { a : 1 } ] ,
1602+ } ) ;
1603+ // defineProperty is neutered, property won't be defined
1604+ expect ( outcome . result [ 0 ] . json . hasProperty ) . toBe ( false ) ;
1605+ expect ( outcome . result [ 0 ] . json . value ) . toBeUndefined ( ) ;
1606+ } ,
1607+ ) ;
1608+
1609+ test . each ( [ [ 'runOnceForAllItems' ] , [ 'runOnceForEachItem' ] ] as const ) (
1610+ 'should neuter Object.defineProperties in %s mode' ,
1611+ async ( mode ) => {
1612+ const execute = mode === 'runOnceForAllItems' ? executeForAllItems : executeForEachItem ;
1613+ const outcome = await execute ( {
1614+ code :
1615+ mode === 'runOnceForAllItems'
1616+ ? `
1617+ const obj = {};
1618+ Object.defineProperties(obj, { a: { value: 1 }, b: { value: 2 } });
1619+ return [{ json: { hasA: 'a' in obj, hasB: 'b' in obj } }];
1620+ `
1621+ : `
1622+ const obj = {};
1623+ Object.defineProperties(obj, { a: { value: 1 }, b: { value: 2 } });
1624+ return { json: { hasA: 'a' in obj, hasB: 'b' in obj } };
1625+ ` ,
1626+ inputItems : [ { a : 1 } ] ,
1627+ } ) ;
1628+ expect ( outcome . result [ 0 ] . json . hasA ) . toBe ( false ) ;
1629+ expect ( outcome . result [ 0 ] . json . hasB ) . toBe ( false ) ;
1630+ } ,
1631+ ) ;
1632+ } ) ;
1633+
15031634 describe ( 'stack trace' , ( ) => {
15041635 it ( 'should extract correct line number from user-defined function stack trace' , async ( ) => {
15051636 const runner = createRunnerWithOpts ( { } ) ;
0 commit comments