blob: 2f44ce276c5d03c32920543f473fd1c415b3280d [file] [log] [blame]
import * as ts from 'typescript';
import {findInChildren} from './ast_tools';
/**
* Determines if the given ts.Node is literal enough for security purposes. This
* is true when the value is built from compile-time constants, with a certain
* tolerance for indirection in order to make this more user-friendly.
*
* This considers a few different things. We accept
* - What TS deems literal (literal strings, literal numbers, literal booleans,
* enum literals),
* - Binary operations of two expressions that we accept (including
* concatenation),
* - Template interpolations of what we accept,
* - `x?y:z` constructions, if we accept `y` and `z`
* - Variables that are const, and initialized with an expression we accept
*
* And to prevent bypasses, expressions that include casts are not accepted, and
* this checker does not follow imports.
*/
export function isLiteral(typeChecker: ts.TypeChecker, node: ts.Node): boolean {
if (ts.isBinaryExpression(node) &&
node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
// Concatenation is fine, if the parts are literals.
return (
isLiteral(typeChecker, node.left) &&
isLiteral(typeChecker, node.right));
} else if (ts.isTemplateExpression(node)) {
// Same for template expressions.
return node.templateSpans.every(span => {
return isLiteral(typeChecker, span.expression);
});
} else if (ts.isTemplateLiteral(node)) {
// and literals (in that order).
return true;
} else if (ts.isConditionalExpression(node)) {
return isLiteral(typeChecker, node.whenTrue) &&
isLiteral(typeChecker, node.whenFalse);
} else if (ts.isIdentifier(node)) {
return isUnderlyingValueAStringLiteral(node, typeChecker);
}
const hasCasts = findInChildren(node, ts.isAsExpression);
return !hasCasts && isLiteralAccordingToItsType(typeChecker, node);
}
/**
* Given an identifier, this function goes around the AST to determine
* whether we should consider it a string literal, on a best-effort basis. It
* is an approximation, but should never have false positives.
*/
function isUnderlyingValueAStringLiteral(
identifier: ts.Identifier, tc: ts.TypeChecker) {
// The identifier references a value, and we try to follow the trail: if we
// find a variable declaration for the identifier, and it was declared as a
// const (so we know it wasn't altered along the way), then the value used
// in the declaration is the value our identifier references. That means we
// should look at the value used in its initialization (by applying the same
// rules as before).
// Since we're best-effort, if a part of that operation failed due to lack
// of support (for instance, the identifier was imported), then we fail
// closed and don't consider the value a literal.
// TODO(rjamet): This doesn't follow imports, which is a feature that we need
// in a fair amount of cases.
return getVariableDeclarationsInSameFile(identifier, tc)
.filter(isConst)
.some(d => d.initializer !== undefined && isLiteral(tc, d.initializer));
}
/**
* Returns whether this thing is a literal based on TS's understanding. This is
* only looking at the local type, so there's no magic in that function.
*/
function isLiteralAccordingToItsType(
typeChecker: ts.TypeChecker, node: ts.Node): boolean {
const nodeType = typeChecker.getTypeAtLocation(node);
return (nodeType.flags &
(ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral |
ts.TypeFlags.BooleanLiteral | ts.TypeFlags.EnumLiteral)) !== 0;
}
/**
* Follows the symbol behind the given identifier, assuming it is a variable,
* and return all the variable declarations we can find that match it in the
* same file.
*/
function getVariableDeclarationsInSameFile(
node: ts.Identifier, tc: ts.TypeChecker): ts.VariableDeclaration[] {
const symbol = tc.getSymbolAtLocation(node);
if (!symbol) {
return [];
}
const decls = symbol.getDeclarations();
if (!decls) {
return [];
}
return decls.filter(ts.isVariableDeclaration);
}
// Tests whether the given variable declaration is Const.
function isConst(varDecl: ts.VariableDeclaration): boolean {
return Boolean(
varDecl && varDecl.parent &&
ts.isVariableDeclarationList(varDecl.parent) &&
varDecl.parent.flags & ts.NodeFlags.Const);
}