@kitajs/ts-html-plugin
Advanced tools
Comparing version 4.0.3 to 4.1.0
@@ -7,3 +7,2 @@ #!/usr/bin/env node | ||
const node_fs_1 = tslib_1.__importDefault(require("node:fs")); | ||
const node_os_1 = require("node:os"); | ||
const node_path_1 = tslib_1.__importDefault(require("node:path")); | ||
@@ -17,3 +16,3 @@ const typescript_1 = tslib_1.__importDefault(require("typescript")); | ||
ts-html-plugin v${version} - A CLI tool & TypeScript LSP for finding XSS vulnerabilities in your TypeScript code. | ||
@kitajs/ts-html-plugin v${version} - A CLI tool & TypeScript LSP for finding XSS vulnerabilities in your TypeScript code. | ||
@@ -110,3 +109,3 @@ Usage: xss-scan [options] <file> <file>... | ||
const simplified = !!(args.simplified || args.s); | ||
const diagnosticFormatter = simplified | ||
const diagnosticFormatter = !process.stdout.isTTY || simplified | ||
? typescript_1.default.formatDiagnostics | ||
@@ -120,5 +119,5 @@ : typescript_1.default.formatDiagnosticsWithColorAndContext; | ||
const diagnosticHost = { | ||
getCurrentDirectory: () => root, | ||
getCurrentDirectory: typescript_1.default.sys.getCurrentDirectory, | ||
getCanonicalFileName: (fileName) => fileName, | ||
getNewLine: () => node_os_1.EOL | ||
getNewLine: () => typescript_1.default.sys.newLine | ||
}; | ||
@@ -125,0 +124,0 @@ if (tsconfig.errors) { |
@@ -18,3 +18,3 @@ "use strict"; | ||
} | ||
const source = program?.getSourceFile(filename); | ||
const source = program.getSourceFile(filename); | ||
if (!source) { | ||
@@ -21,0 +21,0 @@ return diagnostics; |
@@ -7,5 +7,5 @@ import ts, { type JsxFragment } from 'typescript'; | ||
export declare function diagnoseJsxElement(ts: typeof TS, node: JsxElement | JsxFragment, typeChecker: TypeChecker, diagnostics: Diagnostic[]): void; | ||
export declare function isSafeAttribute(ts: typeof TS, type: Type | undefined, expression: ts.Expression, checker: TypeChecker): boolean; | ||
export declare function isSafeAttribute(ts: typeof TS, type: Type | undefined, checker: TypeChecker, node: ts.Node): boolean; | ||
export declare function getSafeAttribute(element: JsxOpeningElement): ts.JsxAttributeLike | undefined; | ||
export declare function proxyObject<T extends object>(obj: T): T; | ||
//# sourceMappingURL=util.d.ts.map |
111
dist/util.js
@@ -49,3 +49,2 @@ "use strict"; | ||
function diagnoseJsxElement(ts, node, typeChecker, diagnostics) { | ||
const file = node.getSourceFile(); | ||
// Validations that does not applies to fragments or serlf closing elements | ||
@@ -80,10 +79,14 @@ if (ts.isJsxElement(node)) { | ||
// has inner expression | ||
exp.expression && | ||
// is expression safe | ||
isSafeAttribute(ts, typeChecker.getTypeAtLocation(exp.expression), exp.expression, typeChecker) && | ||
// does not starts with unsafe | ||
!exp.expression.getText().startsWith('unsafe') && | ||
// Avoids double warnings | ||
!diagnostics.some((d) => d.start === safeAttribute.pos + 1 && d.file === file)) { | ||
diagnostics.push(diagnostic(safeAttribute, 'UnusedSafe', 'Warning')); | ||
exp.expression) { | ||
// gets this expression or array of sub expressions | ||
const expressions = getNodeExpressions(exp.expression) || [exp.expression]; | ||
// at least one jsx inside another jsx with safe | ||
if (expressions.some((inner) => ts.isJsxElement(inner))) { | ||
diagnostics.push(diagnostic(safeAttribute, 'DoubleEscape', 'Error')); | ||
continue; | ||
} | ||
// all of them must be safe | ||
if (expressions.every((inner) => isSafeAttribute(ts, typeChecker.getTypeAtLocation(inner), typeChecker, inner))) { | ||
diagnostics.push(diagnostic(safeAttribute, 'UnusedSafe', 'Warning')); | ||
} | ||
} | ||
@@ -121,34 +124,13 @@ } | ||
} | ||
// Checks both sides | ||
if (ts.isBinaryExpression(node)) { | ||
// Ignores operations which results in a boolean | ||
switch (node.operatorToken.kind) { | ||
case ts.SyntaxKind.EqualsEqualsEqualsToken: | ||
case ts.SyntaxKind.EqualsEqualsToken: | ||
case ts.SyntaxKind.ExclamationEqualsEqualsToken: | ||
case ts.SyntaxKind.ExclamationEqualsToken: | ||
case ts.SyntaxKind.GreaterThanToken: | ||
case ts.SyntaxKind.GreaterThanEqualsToken: | ||
case ts.SyntaxKind.LessThanEqualsToken: | ||
case ts.SyntaxKind.LessThanToken: | ||
case ts.SyntaxKind.InstanceOfKeyword: | ||
case ts.SyntaxKind.InKeyword: | ||
return; | ||
const expressions = getNodeExpressions(node); | ||
// ternary or binary expressions should be evaluated on each side | ||
if (expressions) { | ||
for (const inner of expressions) { | ||
diagnoseExpression(ts, inner, typeChecker, diagnostics, isComponent); | ||
} | ||
// We do not need to evaluate the left side of the expression | ||
// as its value will only be used if its falsy, which cannot have | ||
// XSS content | ||
diagnoseExpression(ts, node.right, typeChecker, diagnostics, isComponent); | ||
return; | ||
} | ||
// Checks the inner expression | ||
if (ts.isConditionalExpression(node)) { | ||
diagnoseExpression(ts, node.whenTrue, typeChecker, diagnostics, isComponent); | ||
diagnoseExpression(ts, node.whenFalse, typeChecker, diagnostics, isComponent); | ||
// ignore node.condition because its value will never be rendered | ||
return; | ||
} | ||
const type = typeChecker.getTypeAtLocation(node); | ||
// Safe can be ignored | ||
if (isSafeAttribute(ts, type, node, typeChecker)) { | ||
if (isSafeAttribute(ts, type, typeChecker, node)) { | ||
return; | ||
@@ -180,3 +162,3 @@ } | ||
} | ||
function isSafeAttribute(ts, type, expression, checker) { | ||
function isSafeAttribute(ts, type, checker, node) { | ||
// Nothing to do if type cannot be resolved | ||
@@ -192,7 +174,8 @@ if (!type) { | ||
// Allows JSX.Element | ||
if (type.aliasSymbol.escapedName === 'Element' && | ||
if (node && | ||
type.aliasSymbol.escapedName === 'Element' && | ||
// @ts-expect-error - Fast way of checking | ||
type.aliasSymbol.parent?.escapedName === 'JSX' && | ||
// Only allows in .map(), other method calls or the expression itself | ||
(ts.isCallExpression(expression) || ts.isIdentifier(expression))) { | ||
(ts.isCallExpression(node) || ts.isIdentifier(node))) { | ||
return true; | ||
@@ -218,9 +201,14 @@ } | ||
// Union types should be checked recursively | ||
if (type.isUnion()) { | ||
return type.types.every((t) => isSafeAttribute(ts, t, expression, checker)); | ||
if (type.isUnionOrIntersection()) { | ||
return type.types.every((innerType) => isSafeAttribute(ts, innerType, checker, node)); | ||
} | ||
// For Array or Promise, we check the type of the first generic | ||
if (checker.isArrayType(type) || type.symbol?.escapedName === 'Promise') { | ||
return isSafeAttribute(ts, type.resolvedTypeArguments?.[0], expression, checker); | ||
return isSafeAttribute(ts, type.resolvedTypeArguments?.[0], checker, node); | ||
} | ||
const text = node.getText(); | ||
// manual unsafe variables should not pass | ||
if (text.startsWith('unsafe')) { | ||
return false; | ||
} | ||
// We allow literal string types here, as if they have XSS content, | ||
@@ -235,3 +223,2 @@ // the user has explicitly written it | ||
} | ||
const text = expression.getText(); | ||
if ( | ||
@@ -263,2 +250,40 @@ // Variables starting with safe are suppressed | ||
} | ||
/** | ||
* Returns more than one node if the node is a binary expression or a conditional | ||
* expression | ||
*/ | ||
function getNodeExpressions(node) { | ||
// Checks operators | ||
if (typescript_1.default.isBinaryExpression(node)) { | ||
// Ignores operations which results in a boolean | ||
if (isBooleanBinaryOperatorToken(node.operatorToken)) { | ||
return []; | ||
} | ||
// Diagnose both sides since both sides can be executed, e.g: | ||
// a empty string in the left side will execute the right side | ||
return [node.left, node.right]; | ||
} | ||
// Checks the inner expression | ||
if (typescript_1.default.isConditionalExpression(node)) { | ||
// ignore node.condition because its value will never be rendered | ||
return [node.whenFalse, node.whenFalse]; | ||
} | ||
return undefined; | ||
} | ||
function isBooleanBinaryOperatorToken(operator) { | ||
switch (operator.kind) { | ||
case typescript_1.default.SyntaxKind.EqualsEqualsEqualsToken: | ||
case typescript_1.default.SyntaxKind.EqualsEqualsToken: | ||
case typescript_1.default.SyntaxKind.ExclamationEqualsEqualsToken: | ||
case typescript_1.default.SyntaxKind.ExclamationEqualsToken: | ||
case typescript_1.default.SyntaxKind.GreaterThanToken: | ||
case typescript_1.default.SyntaxKind.GreaterThanEqualsToken: | ||
case typescript_1.default.SyntaxKind.LessThanEqualsToken: | ||
case typescript_1.default.SyntaxKind.LessThanToken: | ||
case typescript_1.default.SyntaxKind.InstanceOfKeyword: | ||
case typescript_1.default.SyntaxKind.InKeyword: | ||
return true; | ||
} | ||
return false; | ||
} | ||
//# sourceMappingURL=util.js.map |
{ | ||
"name": "@kitajs/ts-html-plugin", | ||
"version": "4.0.3", | ||
"version": "4.1.0", | ||
"homepage": "https://github.com/kitajs/html/tree/master/packages/ts-html-plugin#readme", | ||
@@ -29,4 +29,4 @@ "bugs": "https://github.com/kitajs/html/issues", | ||
"@swc-node/register": "^1.10.9", | ||
"@swc/helpers": "^0.5.12", | ||
"@types/node": "^22.5.2", | ||
"@swc/helpers": "^0.5.13", | ||
"@types/node": "^22.5.5", | ||
"@types/yargs": "^17.0.33", | ||
@@ -38,3 +38,3 @@ "fast-defer": "^1.1.8", | ||
"@kitajs/html": "^4.2.2", | ||
"typescript": "^5.3.3" | ||
"typescript": "^5.6.2" | ||
}, | ||
@@ -41,0 +41,0 @@ "scripts": { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
90275
528