Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ dist/
.globstar/
checkers/registry.go
.idea/
.vscode/
12 changes: 10 additions & 2 deletions analysis/ts_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ var ScopeNodes = []string{
"program",
"arrow_function",
"class_body",
// "class_declaration",
"method_definition",
}

Expand All @@ -50,7 +49,7 @@ func (ts *TsScopeBuilder) DeclaresVariable(node *sitter.Node) bool {
typ == "function_declaration" ||
typ == "method_definition" ||
typ == "class_declaration" ||
typ == "export_statement" || typ == "assignment_expression" || typ == "public_field_definition"
typ == "export_statement" || typ == "assignment_expression" || typ == "public_field_definition" || typ == "call_expression" // To handle cases of inbuilt functions like setTimeout etc.
}

func (ts *TsScopeBuilder) scanDecl(idOrPattern, declarator *sitter.Node, decls []*Variable) []*Variable {
Expand Down Expand Up @@ -245,6 +244,15 @@ func (ts *TsScopeBuilder) CollectVariables(node *sitter.Node) []*Variable {
DeclNode: fieldName,
})
}
case "call_expression":
funcName := node.ChildByFieldName("function")
if funcName != nil && funcName.Type() == "identifier" {
declaredVars = append(declaredVars, &Variable{
Kind: VarKindFunction,
Name: funcName.Content(ts.source),
DeclNode: funcName,
})
}
}

return declaredVars
Expand Down
166 changes: 166 additions & 0 deletions checkers/javascript/eval_express.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package javascript

import (
"slices"

sitter "github.com/smacker/go-tree-sitter"
"globstar.dev/analysis"
)

var EvalExpress = &analysis.Analyzer{
Name: "eval_express",
Language: analysis.LangJs,
Description: "Avoid dynamically evaluating untrusted input, which can lead to a code injection vulnerability.",
Category: analysis.CategorySecurity,
Severity: analysis.SeverityCritical,
Requires: []*analysis.Analyzer{DataFlowAnalyzer},
Run: detectEvalExpress,
}

func detectEvalExpress(pass *analysis.Pass) (interface{}, error) {
dfg := pass.ResultOf[DataFlowAnalyzer].(*DataFlowGraph)
if dfg == nil {
return nil, nil
}

scopeTree := dfg.ScopeTree
if scopeTree == nil {
return nil, nil
}

flowGraph := dfg.Graph
if flowGraph == nil {
return nil, nil
}

funcCall := dfg.FuncCalls
if funcCall == nil {
return nil, nil
}

// Common user-input sources for JavaScript applications.
userInputSrc := []string{"req.query.input", "req.params.id", "req.body", "req.cookies.sessionId", "localStorage.getItem"}

// These methods call eval directly, or under the hood. This leads to vulnerability.
vulnMethods := []string{"eval", "setTimeout", "Function"}
taintedNodes := []*DataFlowNode{}

analysis.Preorder(pass, func(node *sitter.Node) {
if node == nil {
return
}
if len(vulnMethods) < 0 && len(userInputSrc) < 0 {
return
}

if node.Type() == "variable_declarator" {
currScope := scopeTree.GetScope(node)

nameNode := node.ChildByFieldName("name")
valueNode := node.ChildByFieldName("value")

var nameVar *analysis.Variable
if nameNode != nil {
nameVar = currScope.Lookup(nameNode.Content(pass.FileContext.Source))
}

var nameDfNode *DataFlowNode

if dfnode, ok := flowGraph[nameVar]; ok {
nameDfNode = dfnode
}

if valueNode != nil {
switch valueNode.Type() {
// Track and mark any user-input sources, on the data-flow node level.
case "member_expression", "identifier":
valueContent := valueNode.Content(pass.FileContext.Source)
if slices.Contains(userInputSrc, valueContent) {
taintedNodes = append(taintedNodes, nameDfNode)
}

// Check for any possibility of vulnerability
case "call_expression":
funcNode := valueNode.ChildByFieldName("function")
var funcName string
if funcNode != nil {
funcName = funcNode.Content(pass.FileContext.Source)
}
if slices.Contains(vulnMethods, funcName) {
if ContainsAny(nameDfNode.Sources, taintedNodes) {
pass.Report(pass, node, "Eval attempt on user input, code injection vulnerability.")
}
}

case "new_expression":
ctor := valueNode.ChildByFieldName("constructor")
var ctorName string
if ctor != nil {
ctorName = ctor.Content(pass.FileContext.Source)
}
if ctorName == "Function" {
args := valueNode.ChildByFieldName("arguments")
if args != nil {
for i := 0; i < int(args.NamedChildCount()); i++ {
child := args.NamedChild(i)
if child != nil && child.Type() == "template_string" {
for j := 0; j < int(child.NamedChildCount()); j++ {
exprNode := child.NamedChild(j)
if exprNode != nil && exprNode.Type() == "template_substitution" {
idNode := exprNode.NamedChild(0)
if idNode != nil && idNode.Type() == "identifier" {
varName := idNode.Content(pass.FileContext.Source)
if variable := currScope.Lookup(varName); variable != nil {
if slices.Contains(taintedNodes, flowGraph[variable]) {
pass.Report(pass, node, "Eval attempt on user input, code injection vulnerability.")
}
}
}
}

}
}
}
}
}
}

}
}

if node.Type() == "expression_statement" {
callNode := node.NamedChild(0)
funcNode := callNode.ChildByFieldName("function")
var funcName string
if funcNode != nil {
funcName = funcNode.Content(pass.FileContext.Source)
}
if slices.Contains(vulnMethods, funcName) {
if funcCall, ok := functionCalls[callNode]; ok {
if ContainsAny(funcCall.Sources, taintedNodes) {
pass.Report(pass, node, "Eval attempt on user input, code injection vulnerability.")
}
}

}
}

})

return nil, nil
}

func ContainsAny[T comparable](a, b []T) bool {
for _, x := range a {
for _, y := range b {
if x == y {
return true
}
}
}
return false
}

// TODO:
// - [] Add vuln detection for call Expressions.
// - [] Updated DFG to handle New Function() statements, instead of brute-forcing here
99 changes: 74 additions & 25 deletions checkers/javascript/js_dataflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ type FunctionDefinition struct {
Scope *analysis.Scope
}

type FunctionCall struct {
Node *sitter.Node
Sources []*DataFlowNode
DfNode *DataFlowNode
}

type ClassDefinition struct {
Node *sitter.Node
Properties []*analysis.Variable
Expand All @@ -49,10 +55,12 @@ type DataFlowGraph struct {
ScopeTree *analysis.ScopeTree
FuncDefs map[string]*FunctionDefinition
ClassDefs map[*analysis.Variable]*ClassDefinition
FuncCalls map[*sitter.Node]*FunctionCall
}

var functionDefinitions = make(map[string]*FunctionDefinition)
var classDefinitions = make(map[*analysis.Variable]*ClassDefinition)
var functionCalls = make(map[*sitter.Node]*FunctionCall)

// var DataFlowGraph = make(map[*analysis.Variable]*DataFlowNode)

Expand Down Expand Up @@ -85,6 +93,7 @@ func createDataFlowGraph(pass *analysis.Pass) (interface{}, error) {
}

// Track variable declarations and assignments
// TODO: Add sources for the nameNode.
if node.Type() == "variable_declarator" || node.Type() == "assignment_expression" {
var nameNode, valueNode *sitter.Node

Expand Down Expand Up @@ -175,6 +184,7 @@ func createDataFlowGraph(pass *analysis.Pass) (interface{}, error) {
dfNode.FuncDef = funcDef

}

dataFlowGraph.Graph[variable] = dfNode

}
Expand Down Expand Up @@ -216,36 +226,66 @@ func createDataFlowGraph(pass *analysis.Pass) (interface{}, error) {
immidiateFunc := node.ChildByFieldName("function")
// Used to verify that the call_expression is actually pointing to an IIFE(immidiately invoked function expression)
// also filters out false positives of regular call expressions like console.log(), foo(x) etc.
if immidiateFunc == nil || immidiateFunc.Type() != "parenthesized_expression" {
return
}
if immidiateFunc != nil && immidiateFunc.Type() == "parenthesized_expression" {
funcExpr := immidiateFunc.NamedChild(0)
if funcExpr == nil {
return
}

funcExpr := immidiateFunc.NamedChild(0)
if funcExpr == nil {
return
}
funcDef := &FunctionDefinition{
Node: funcExpr,
Body: funcExpr.ChildByFieldName("body"),
Scope: currentScope,
}

funcDef := &FunctionDefinition{
Node: funcExpr,
Body: funcExpr.ChildByFieldName("body"),
Scope: currentScope,
}
params := node.ChildByFieldName("parameters")
if params != nil {
for i := 0; i < int(params.NamedChildCount()); i++ {
param := params.NamedChild(i)
if param.Type() == "identifier" {
paramName := param.Content(pass.FileContext.Source)
paramVar := currentScope.Lookup(paramName)
if paramVar != nil {
funcDef.Parameters = append(funcDef.Parameters, paramVar)
}

params := node.ChildByFieldName("parameters")
if params != nil {
for i := 0; i < int(params.NamedChildCount()); i++ {
param := params.NamedChild(i)
if param.Type() == "identifier" {
paramName := param.Content(pass.FileContext.Source)
paramVar := currentScope.Lookup(paramName)
if paramVar != nil {
funcDef.Parameters = append(funcDef.Parameters, paramVar)
}
}
}
// Create a data flow node for the IIFE
}
if immidiateFunc != nil && immidiateFunc.Type() == "identifier" {

funcname := immidiateFunc.Content(pass.FileContext.Source)

_, exists := functionDefinitions[funcname]
if !exists {
funcVar := currentScope.Lookup(funcname)
if funcVar == nil {
return
}
dfNode := &DataFlowNode{
Node: immidiateFunc,
Sources: []*DataFlowNode{},
Scope: currentScope,
Variable: funcVar,
}

handleCallExpressionDataFlow(node, dfNode, dataFlowGraph.Graph, pass.FileContext.Source, currentScope)

functionCalls[node] = &FunctionCall{
Node: immidiateFunc,
Sources: dfNode.Sources,
DfNode: dfNode,
}

}
}
// Create a data flow node for the IIFE

// Need a way to track function calls like setTimeout etc.
// Can do something like this:
// check if the function exits in the function definitions map. If it doesn't exist, then it must be a function call specific to the language.
// Then handle it accordingly, for sources etc.
}

if node.Type() == "class_declaration" {
Expand Down Expand Up @@ -325,7 +365,6 @@ func createDataFlowGraph(pass *analysis.Pass) (interface{}, error) {
}
}

fmt.Println(classChild)
}
}

Expand All @@ -346,6 +385,7 @@ func createDataFlowGraph(pass *analysis.Pass) (interface{}, error) {
})
dataFlowGraph.FuncDefs = functionDefinitions
dataFlowGraph.ClassDefs = classDefinitions
dataFlowGraph.FuncCalls = functionCalls

return dataFlowGraph, nil
}
Expand Down Expand Up @@ -414,7 +454,6 @@ func handleTemplateStringDataFlow(node *sitter.Node, dfNode *DataFlowNode, DataF
if variable := scope.Lookup(varName); variable != nil {
if sourceNode, exists := DataFlowGraph[variable]; exists {
dfNode.Sources = append(dfNode.Sources, sourceNode)

}
}
}
Expand All @@ -427,7 +466,6 @@ func handleCallExpressionDataFlow(node *sitter.Node, dfNode *DataFlowNode, DataF
if node == nil || node.Type() != "call_expression" {
return
}

args := node.ChildByFieldName("arguments")
if args == nil {
return
Expand All @@ -436,6 +474,7 @@ func handleCallExpressionDataFlow(node *sitter.Node, dfNode *DataFlowNode, DataF
// Check each argument for taint
for i := 0; i < int(args.NamedChildCount()); i++ {
arg := args.NamedChild(i)

if arg == nil {
continue
}
Expand All @@ -447,7 +486,17 @@ func handleCallExpressionDataFlow(node *sitter.Node, dfNode *DataFlowNode, DataF
dfNode.Sources = append(dfNode.Sources, sourceNode)
}
}
}

// Add handling of template strings inside a function call
if arg.Type() == "template_string" {
// fmt.Println(arg.Content(sourceCode))
handleTemplateStringDataFlow(arg, dfNode, DataFlowGraph, sourceCode, scope)
}

if arg.Type() == "binary_expression" {
handleBinaryExpressionDataFlow(arg, dfNode, DataFlowGraph, sourceCode, scope)
}
}

}
Loading