You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@endo/evasive-transform

Package Overview
Dependencies
Maintainers
7
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@endo/evasive-transform - npm Package Compare versions

Comparing version
2.0.2
to
2.1.0
+5
src/transform-code.d.ts
export function evadeStrings(p: any): void;
export function evadeTemplates(p: any): void;
export function evadeRegexpLiteral(p: any): void;
export function evadeDecrementGreater(p: any): void;
//# sourceMappingURL=transform-code.d.ts.map
{"version":3,"file":"transform-code.d.ts","sourceRoot":"","sources":["transform-code.js"],"names":[],"mappings":"AA2DO,2CAsBN;AAQM,6CA0FN;AAWM,4CAFM,IAAI,CAehB;AASM,+CAFM,IAAI,CAiBhB"}
const evadeRegexp = /import\s*\(|<!--|-->/g;
// The replacement collection for regexp patterns matching the evadeRegexp is only applied to the first matched character, so it is necessary for the regexpReplacements to be maintained together with the evadeRegexp.
const regexpReplacements = {
i: '\\x69',
'<': '\\x3C',
'-': '\\x2D',
};
/**
* Copy the location from one AST node to another (round-tripping through JSON
* to sever references), updating the target's end position as if it had zero
* length.
*
* @param {import('@babel/types').Node} target
* @param {import('@babel/types').Node} src
*/
const adoptStartFrom = (target, src) => {
try {
const srcLoc = src.loc;
if (!srcLoc) return;
const loc = /** @type {typeof srcLoc} */ (
JSON.parse(JSON.stringify(srcLoc))
);
const start = loc?.start;
target.loc = loc;
// Text of the new node is likely shorter than text of the old (e.g.,
// "import(<url>)" -> "im"), and in such cases we don't ever want rendering
// of the new node to claim too much real estate so we future-proof by
// making it appear to be zero-width and trusting in recovery of the actual
// location immediately afterwards.
if (start) target.loc.end = /** @type {typeof start} */ ({ ...start });
} catch (_err) {
// Ignore errors; this is purely opportunistic.
}
};
/**
* Creates a BinaryExpression adding two expressions
*
* @param {import('@babel/types').Expression} left
* @param {string} rightString
* @returns {import('@babel/types').BinaryExpression}
*/
const addStringToExpressions = (left, rightString) => ({
type: 'BinaryExpression',
operator: '+',
left,
right: {
type: 'StringLiteral',
value: rightString,
},
});
/**
* Break up problematic substrings into concatenation expressions, e.g.
* `"import("` -> `"im"+"port("`.
*
* @param {import('@babel/traverse').NodePath} p
*/
export const evadeStrings = p => {
const { node } = p;
if (node.type !== 'StringLiteral') {
return;
}
const { value } = node;
/** @type {import('@babel/types').Expression | undefined} */
let expr;
let lastIndex = 0;
for (const match of value.matchAll(evadeRegexp)) {
const index = match.index + 2;
const part = value.substring(lastIndex, index);
expr = !expr
? { type: 'StringLiteral', value: part }
: addStringToExpressions(expr, part);
if (lastIndex === 0) adoptStartFrom(expr, p.node);
lastIndex = index;
}
if (expr) {
expr = addStringToExpressions(expr, value.substring(lastIndex));
p.replaceWith(expr);
}
};
/**
* Break up problematic substrings in template literals with empty-string
* expressions, e.g. `import(` -> `im${''}port(`.
*
* @param {import('@babel/traverse').NodePath} p
*/
export const evadeTemplates = p => {
/** @type {import('@babel/types').TemplateLiteral} */
const node = p.node;
// The transform is only meaning-preserving if not part of a
// TaggedTemplateExpression, so these need to be excluded until a motivating
// case shows up. It should be possible to wrap the tag with a function that
// omits expressions we insert, but that's a lot of work to do preemptively.
// https://github.com/endojs/endo/pull/3026#discussion_r2632507228
if (
node.type !== 'TemplateLiteral' ||
p.parent.type === 'TaggedTemplateExpression'
) {
return;
}
const { quasis } = node;
// Check if any quasi needs transformation
if (!quasis.some(quasi => quasi.value.raw.search(evadeRegexp) !== -1)) return;
/** @type {import('@babel/types').TemplateElement[]} */
const newQuasis = [];
/** @type {import('@babel/types').Expression[]} */
const newExpressions = [];
const addQuasi = quasiValue => {
// Insert empty expression to break the pattern
newExpressions.push({
type: 'StringLiteral',
value: '',
});
// Add chunk from lastIndex to nextSplitIndex
newQuasis.push({
type: 'TemplateElement',
value: {
raw: quasiValue,
cooked: quasiValue,
},
tail: false,
});
};
for (let i = 0; i < quasis.length; i += 1) {
const quasi = quasis[i];
// We're not currently preserving raw vs. cooked literal data.
const quasiValue = quasi.value.raw;
let lastIndex = 0;
for (const match of quasiValue.matchAll(evadeRegexp)) {
const index = match.index + 2;
const raw = quasiValue.substring(lastIndex, index);
if (lastIndex === 0) {
// Literal text up to our first cut point.
newQuasis.push({
type: 'TemplateElement',
value: { raw, cooked: raw },
tail: false,
});
} else {
addQuasi(raw);
}
lastIndex = index;
}
if (lastIndex !== 0) {
addQuasi(quasiValue.substring(lastIndex));
} else {
newQuasis.push(quasi);
}
// Add original expression between quasis
if (i < node.expressions.length) {
// @ts-ignore whatever was there, must still be allowed.
newExpressions.push(node.expressions[i]);
}
}
// Mark last quasi as tail
if (newQuasis.length > 0) {
newQuasis[newQuasis.length - 1].tail = true;
}
/** @type {import('@babel/types').Node} */
const replacement = {
type: 'TemplateLiteral',
quasis: newQuasis,
expressions: newExpressions,
};
adoptStartFrom(replacement, p.node);
p.replaceWith(replacement);
};
/**
* Transforms RegExp literals containing "import" to use a character class
* to break the pattern detection.
*
* `/import(/` -> `/im[p]ort(/`
*
* @param {import('@babel/traverse').NodePath} p
* @returns {void}
*/
export const evadeRegexpLiteral = p => {
const { node } = p;
if (node.type !== 'RegExpLiteral') {
return;
}
const { pattern } = node;
if (pattern.search(evadeRegexp) !== -1) {
node.pattern = pattern.replace(
evadeRegexp,
s => regexpReplacements[s[0]] + s.substring(1),
);
}
};
/**
* Prevents `-->` from appearing in output by transforming
* `x-->y` to `(0,x--)>y`.
*
* @param {import('@babel/traverse').NodePath} p
* @returns {void}
*/
export const evadeDecrementGreater = p => {
const { node } = p;
if (
node.type === 'BinaryExpression' &&
node.operator === '>' &&
node.left.type === 'UpdateExpression' &&
node.left.operator === '--' &&
!node.left.prefix
) {
// Wrap the UpdateExpression in a SequenceExpression: (0, x--)
node.left = {
type: 'SequenceExpression',
expressions: [{ type: 'NumericLiteral', value: 0 }, node.left],
};
}
};
+13
-15
{
"name": "@endo/evasive-transform",
"version": "2.0.2",
"version": "2.1.0",
"description": "Source transforms to evade SES censorship",

@@ -29,3 +29,3 @@ "keywords": [

"test": "ava",
"test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js",
"test:c8": "c8 ${C8_OPTIONS:-} ava",
"test:xs": "exit 0",

@@ -42,11 +42,9 @@ "build": "exit 0",

"devDependencies": {
"@babel/types": "~7.26.0",
"@endo/ses-ava": "^1.3.2",
"@types/babel__generator": "^7.6.8",
"@types/babel__traverse": "^7.20.6",
"ava": "^6.1.3",
"c8": "^7.14.0",
"eslint": "^8.57.0",
"tsd": "^0.31.2",
"typescript": "~5.8.3"
"@babel/types": "~7.28.2",
"@endo/ses-ava": "^1.4.0",
"ava": "catalog:dev",
"c8": "catalog:dev",
"eslint": "catalog:dev",
"tsd": "catalog:dev",
"typescript": "~5.9.2"
},

@@ -79,7 +77,7 @@ "files": [

"dependencies": {
"@babel/generator": "^7.26.3",
"@babel/parser": "~7.26.2",
"@babel/traverse": "~7.25.9"
"@babel/generator": "^7.28.3",
"@babel/parser": "~7.28.3",
"@babel/traverse": "~7.28.3"
},
"gitHead": "9815aea9541f241389d2135c6097a7442bdffa17"
"gitHead": "f91329e8616a19f131d009356a5f11ef11c839cc"
}

@@ -5,6 +5,32 @@ # @endo/evasive-transform

This package provides a function which transforms comments contained in source code which would otherwise be rejected outright by SES.
This package provides a function which transforms source code which would otherwise be rejected outright by SES.
The transform is meaning-preserving.
## Example
It covers sequences resembling HTML comments and dynamic `import` inside of:
- comments
- strings
- template strings (but not tagged template strings)
- regular expression literals
and additionally covers sequences resembling HTML comments inside of code itself (e.g., `if (a-->b)`).
## Usage
### Options
Both `evadeCensor` and `evadeCensorSync` accept a source string as the first argument and an options object as the second argument:
| Option | Type | Description |
|--------|------|-------------|
| `sourceUrl` | `string` | The URL or filename of the source file. Used for source map generation and error messages. |
| `sourceMap` | `string \| object` | Optional. An existing source map (as JSON string or object) to be updated with the transform's mappings. |
| `sourceType` | `'script' \| 'module'` | Optional. Specifies whether the source is a CommonJS script (`'script'`) or an ES module (`'module'`). When provided, it helps the parser handle the code correctly. |
| `elideComments` | `boolean` | Optional. If `true`, removes comment contents while preserving newlines. Defaults to `false`. |
| `onlyComments` | `boolean` | Optional. If `true`, limits transformation to comment contents only, leaving code unchanged. Defaults to `false`. |
### Example
See example below, or see the second test in [packages/compartment-mapper/test/evasive-transform.test.js](../compartment-mapper/test/evasive-transform.test.js) for a demonstration of its usage in compartment-mapper.
```js

@@ -29,6 +55,8 @@ // ESM example

sourceType,
elideComments: true,
onlyComments: true,
});
/**
* The resulting file will now contain `@property {ІᛖРΟᏒТ('foo').Bar} bar`, which SES will allow (and TypeScript no longer understands, but that should be fine for the use-case).
* The resulting file will now contain `@property {IMPORT('foo').Bar} bar`, which SES will allow (and TypeScript no longer understands, but that should be fine for the use-case).
*

@@ -35,0 +63,0 @@ * Note that this could be avoided entirely by stripping comments during, say, a bundling phase.

@@ -56,4 +56,4 @@ /**

export type TransformedResultWithSourceMap = TransformedResult & {
map: NonNullable<import("@babel/generator").GeneratorResult["map"]>;
map: NonNullable<any["map"]>;
};
//# sourceMappingURL=generate.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["generate.js"],"names":[],"mappings":";;;;;;;;AAwDE,8BACS,OAAO,cAAc,EAAE,IAAI,WAC3B,+BAA+B,GAC7B,8BAA8B,CACxC;;;;;;;;;AAKD,8BACS,OAAO,cAAc,EAAE,IAAI,6CAEzB,iBAAiB,CAC3B;;;;;;;;;eA7CW,MAAM;;;;;;;;;;;;;;gBAUN,SAAS;;;;;;gCAQV;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,SAAS,CAAA;CAAC;;;;;6CAQ9B,iBAAiB,GAAG;IAAE,GAAG,EAAE,WAAW,CAAC,OAAO,kBAAkB,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,CAAA;CAAC"}
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["generate.js"],"names":[],"mappings":";;;;;;;;AAwDE,8BACS,OAAO,cAAc,EAAE,IAAI,WAC3B,+BAA+B,GAC7B,8BAA8B,CACxC;;;;;;;;;AAKD,8BACS,OAAO,cAAc,EAAE,IAAI,6CAEzB,iBAAiB,CAC3B;;;;;;;;;eA7CW,MAAM;;;;;;;;;;;;;;gBAUN,SAAS;;;;;;gCAQV;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,SAAS,CAAA;CAAC;;;;;6CAQ9B,iBAAiB,GAAG;IAAE,GAAG,EAAE,WAAW,CAAC,GAA0C,CAAC,KAAK,CAAC,CAAC,CAAA;CAAC"}

@@ -92,3 +92,2 @@ /**

sourceMaps: Boolean(sourceUrl),
// @ts-expect-error Property missing on versioned types
inputSourceMap,

@@ -95,0 +94,0 @@ retainLines: true,

@@ -70,3 +70,3 @@ /**

/**
* - Replace comments with an ellipsis but preserve interior newlines.
* - Empties the comments but preserves interior newlines.
*/

@@ -79,2 +79,7 @@ elideComments?: boolean | undefined;

/**
* - if true, will limit transformation to
* comment contents, preserving code positions within each line
*/
onlyComments?: boolean | undefined;
/**
* - deprecated, vestigial

@@ -81,0 +86,0 @@ */

@@ -1,1 +0,1 @@

{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":";;;;;;;;;;;;AAgCG,wCACQ,MAAM,WACN,kBAAkB,GAAG;IAAC,SAAS,EAAE,MAAM,CAAA;CAAC,GACtC,8BAA8B,CACxC;;;;;;;;;;;;;AASA,wCACQ,MAAM,6CAEJ,iBAAiB,CAC3B;;;;;;;;;;;;;AAwCA,oCACQ,MAAM,WACN,kBAAkB,GAAG;IAAC,SAAS,EAAE,MAAM,CAAA;CAAC,GACtC,OAAO,CAAC,8BAA8B,CAAC,CACjD;;;;;;;;;;;;;AASA,oCACQ,MAAM,6CAEJ,OAAO,CAAC,iBAAiB,CAAC,CACpC;;;;;;;;;;;;;;;;;;;;;;;;;;oDAnGiE,eAAe;uCAAf,eAAe"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":";;;;;;;;;;;;AAkCG,wCACQ,MAAM,WACN,kBAAkB,GAAG;IAAC,SAAS,EAAE,MAAM,CAAA;CAAC,GACtC,8BAA8B,CACxC;;;;;;;;;;;;;AASA,wCACQ,MAAM,6CAEJ,iBAAiB,CAC3B;;;;;;;;;;;;;AAyCA,oCACQ,MAAM,WACN,kBAAkB,GAAG;IAAC,SAAS,EAAE,MAAM,CAAA;CAAC,GACtC,OAAO,CAAC,8BAA8B,CAAC,CACjD;;;;;;;;;;;;;AASA,oCACQ,MAAM,6CAEJ,OAAO,CAAC,iBAAiB,CAAC,CACpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oDAtGiE,eAAe;uCAAf,eAAe"}

@@ -21,4 +21,6 @@ /**

* @property {string} [sourceUrl] - URL or filepath of the original source in `code`
* @property {boolean} [elideComments] - Replace comments with an ellipsis but preserve interior newlines.
* @property {boolean} [elideComments] - Empties the comments but preserves interior newlines.
* @property {import('./parse-ast.js').SourceType} [sourceType] - Module source type
* @property {boolean} [onlyComments] - if true, will limit transformation to
comment contents, preserving code positions within each line
* @property {boolean} [useLocationUnmap] - deprecated, vestigial

@@ -69,2 +71,3 @@ * @public

elideComments = false,
onlyComments = false,
} = options || {};

@@ -78,3 +81,3 @@

transformAst(ast, { elideComments });
transformAst(ast, { elideComments, onlyComments });

@@ -81,0 +84,0 @@ if (sourceUrl) {

@@ -20,5 +20,6 @@ /**

* @param {ParseAstOptions} [opts] - Options for underlying parser
* @returns {any}
* @internal
*/
export function parseAst(source: string, opts?: ParseAstOptions): babelParser.ParseResult<import("@babel/types").File>;
export function parseAst(source: string, opts?: ParseAstOptions): any;
/**

@@ -35,3 +36,2 @@ * This is a subset of `@babel/parser`'s `ParserOptions['sourceType']`, but

};
import * as babelParser from '@babel/parser';
//# sourceMappingURL=parse-ast.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"parse-ast.d.ts","sourceRoot":"","sources":["parse-ast.js"],"names":[],"mappings":"AAUA;;;;;;GAMG;AAEH;;;;;;GAMG;AAEH;;;;;;GAMG;AACH,iCAJW,MAAM,SACN,eAAe,wDAWzB;;;;;yBA3BY,QAAQ,GAAG,QAAQ;;;;;;;6BARH,eAAe"}
{"version":3,"file":"parse-ast.d.ts","sourceRoot":"","sources":["parse-ast.js"],"names":[],"mappings":"AAUA;;;;;;GAMG;AAEH;;;;;;GAMG;AAGH;;;;;;;GAOG;AACH,iCALW,MAAM,SACN,eAAe,GACb,GAAG,CAWf;;;;;yBA7BY,QAAQ,GAAG,QAAQ"}

@@ -27,2 +27,3 @@ /**

// XXX returns `any` to work around: The inferred type of 'parseAst' cannot be named without a reference to '@babel/parser/node_modules/@babel/types'. This is likely not portable. A type annotation is necessary.
/**

@@ -33,2 +34,3 @@ * Adapter for parsing an AST.

* @param {ParseAstOptions} [opts] - Options for underlying parser
* @returns {any}
* @internal

@@ -35,0 +37,0 @@ */

@@ -13,2 +13,3 @@ /**

* @property {boolean} [elideComments]
* @property {boolean} [onlyComments]
*/

@@ -25,3 +26,3 @@ /**

*/
export function transformAst(ast: import("@babel/types").File, { elideComments }?: TransformAstOptions): void;
export function transformAst(ast: import("@babel/types").File, { elideComments, onlyComments }?: TransformAstOptions): void;
/**

@@ -36,3 +37,4 @@ * Options for {@link transformAst}

elideComments?: boolean | undefined;
onlyComments?: boolean | undefined;
};
//# sourceMappingURL=transform-ast.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"transform-ast.d.ts","sourceRoot":"","sources":["transform-ast.js"],"names":[],"mappings":"AAkBA;;;;;GAKG;AAEH;;;;;;GAMG;AAEH;;;;;;;;;GASG;AACH,kCAJW,OAAO,cAAc,EAAE,IAAI,sBAC3B,mBAAmB,GACjB,IAAI,CAuBhB;;;;kCA1CY,mCAAmC"}
{"version":3,"file":"transform-ast.d.ts","sourceRoot":"","sources":["transform-ast.js"],"names":[],"mappings":"AAwBA;;;;;GAKG;AAEH;;;;;;;GAOG;AAEH;;;;;;;;;GASG;AACH,kCAJW,OAAO,cAAc,EAAE,IAAI,oCAC3B,mBAAmB,GACjB,IAAI,CA6BhB;;;;kCAjDY,mCAAmC"}

@@ -9,2 +9,8 @@ /**

import { evadeComment, elideComment } from './transform-comment.js';
import {
evadeStrings,
evadeTemplates,
evadeDecrementGreater,
evadeRegexpLiteral,
} from './transform-code.js';

@@ -33,2 +39,3 @@ // TODO The following is sufficient on Node.js, but for compatibility with

* @property {boolean} [elideComments]
* @property {boolean} [onlyComments]
*/

@@ -46,3 +53,6 @@

*/
export function transformAst(ast, { elideComments = false } = {}) {
export function transformAst(
ast,
{ elideComments = false, onlyComments = false } = {},
) {
const transformComment = elideComments ? elideComment : evadeComment;

@@ -58,6 +68,3 @@ traverse(ast, {

(leadingComments || []).forEach(node => transformComment(node));
// XXX: there is no such Node having type matching /^Comment.+/ in
// @babel/types
if (type.startsWith('Comment')) {
// @ts-expect-error - see above XXX
transformComment(p.node);

@@ -67,4 +74,10 @@ }

(trailingComments || []).forEach(node => transformComment(node));
if (!onlyComments) {
evadeStrings(p);
evadeTemplates(p);
evadeRegexpLiteral(p);
evadeDecrementGreater(p);
}
},
});
}