Skip to content

Commit

Permalink
Update command injection analyzer logic
Browse files Browse the repository at this point in the history
  • Loading branch information
SachinAkash01 committed Feb 24, 2025
1 parent e828f74 commit c9cc375
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"location": {
"filePath": "main.bal",
"startLine": 23,
"endLine": 26,
"startColumn": 30,
"endColumn": 6,
"startOffset": 868,
"length": 68
},
"rule": {
"id": "ballerina/os:1",
"numericId": 1,
"description": "Avoid constructing system command arguments from user input without proper sanitization",
"ruleKind": "VULNERABILITY"
},
"source": "BUILT_IN",
"fileName": "rule1/main.bal",
"filePath": "/Users/sachink/Desktop/module-ballerina-os/module-ballerina-os/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/main.bal"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ public class Constants {
private Constants() {}

public static final String SCANNER_CONTEXT = "ScannerContext";
public static final String PUBLIC_QUALIFIER = "public";
public static final String OS = "os";
public static final String EXEC = "exec";
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,7 @@

package io.ballerina.stdlib.os.compiler;

import io.ballerina.compiler.syntax.tree.FunctionCallExpressionNode;
import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode;
import io.ballerina.projects.Document;
import io.ballerina.projects.Module;
import io.ballerina.projects.Package;
import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext;

public class OSCompilerPluginUtil {

private OSCompilerPluginUtil() {}

public static boolean isOsExecCall(SyntaxNodeAnalysisContext context, FunctionCallExpressionNode functionCall) {
if (!(functionCall.functionName() instanceof SimpleNameReferenceNode functionNameNode)) {
return false;
}

String functionName = functionNameNode.name().text();
return functionName.equals("exec") && isOsModule(context);
}

private static boolean isOsModule(SyntaxNodeAnalysisContext context) {
Package currentPackage = context.currentPackage();
return currentPackage != null && currentPackage.moduleIds().stream()
.map(currentPackage::module)
.map(Module::moduleName)
.anyMatch(name -> name.toString().equals("os"));
}

public static Document getDocument(SyntaxNodeAnalysisContext context) {
return context.currentPackage().module(context.moduleId()).document(context.documentId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,38 @@
import io.ballerina.compiler.api.SemanticModel;
import io.ballerina.compiler.api.symbols.Symbol;
import io.ballerina.compiler.api.symbols.SymbolKind;
import io.ballerina.compiler.syntax.tree.AssignmentStatementNode;
import io.ballerina.compiler.syntax.tree.ExpressionNode;
import io.ballerina.compiler.syntax.tree.FunctionArgumentNode;
import io.ballerina.compiler.syntax.tree.FunctionBodyBlockNode;
import io.ballerina.compiler.syntax.tree.FunctionBodyNode;
import io.ballerina.compiler.syntax.tree.FunctionCallExpressionNode;
import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode;
import io.ballerina.compiler.syntax.tree.ListConstructorExpressionNode;
import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode;
import io.ballerina.compiler.syntax.tree.MappingFieldNode;
import io.ballerina.compiler.syntax.tree.NamedArgumentNode;
import io.ballerina.compiler.syntax.tree.Node;
import io.ballerina.compiler.syntax.tree.PositionalArgumentNode;
import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode;
import io.ballerina.compiler.syntax.tree.RequiredParameterNode;
import io.ballerina.compiler.syntax.tree.SeparatedNodeList;
import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode;
import io.ballerina.compiler.syntax.tree.SpecificFieldNode;
import io.ballerina.compiler.syntax.tree.VariableDeclarationNode;
import io.ballerina.projects.Document;
import io.ballerina.projects.plugins.AnalysisTask;
import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext;
import io.ballerina.scan.Reporter;
import io.ballerina.stdlib.os.compiler.OSCompilerPluginUtil;
import io.ballerina.tools.diagnostics.Location;

import static io.ballerina.stdlib.os.compiler.OSCompilerPluginUtil.isOsExecCall;
import static io.ballerina.stdlib.os.compiler.Constants.EXEC;
import static io.ballerina.stdlib.os.compiler.Constants.OS;
import static io.ballerina.stdlib.os.compiler.Constants.PUBLIC_QUALIFIER;
import static io.ballerina.stdlib.os.compiler.staticcodeanalyzer.OSRule.AVOID_UNSANITIZED_CMD_ARGS;

public class OSCommandInjectionAnalyzer implements AnalysisTask<SyntaxNodeAnalysisContext> {

private final Reporter reporter;

public OSCommandInjectionAnalyzer(Reporter reporter) {
Expand All @@ -49,23 +65,66 @@ public void perform(SyntaxNodeAnalysisContext context) {
return;
}

if (!isOsExecCall(context, functionCall)) {
if (!isOsExecCall(functionCall)) {
return;
}

Document document = OSCompilerPluginUtil.getDocument(context);
Document document = getDocument(context);

if (containsUserControlledInput(functionCall.arguments(), context)) {
Location location = functionCall.location();
this.reporter.reportIssue(document, location, AVOID_UNSANITIZED_CMD_ARGS.getId());
}
}

public static boolean isOsExecCall(FunctionCallExpressionNode functionCall) {
if (!(functionCall.functionName() instanceof QualifiedNameReferenceNode qNode)) {
return false;
}
return qNode.modulePrefix().text().equals(OS) && qNode.identifier().text().equals(EXEC);
}

public static Document getDocument(SyntaxNodeAnalysisContext context) {
return context.currentPackage().module(context.moduleId()).document(context.documentId());
}

private boolean containsUserControlledInput(SeparatedNodeList<FunctionArgumentNode> arguments,
SyntaxNodeAnalysisContext context) {
for (Node arg : arguments) {
if (isUserControlledInput(arg, context)) {
return true;
for (FunctionArgumentNode arg : arguments) {
// Extract the expression inside the argument node
ExpressionNode expr;
if (arg instanceof PositionalArgumentNode posArg) {
expr = posArg.expression();
} else if (arg instanceof NamedArgumentNode namedArg) {
expr = namedArg.expression();
} else {
continue;
}

// Check if the extracted expression is a record (mapping constructor)
if (expr instanceof MappingConstructorExpressionNode mappingNode) {
for (MappingFieldNode field : mappingNode.fields()) {
if (field instanceof SpecificFieldNode specificField) {
String fieldName = specificField.fieldName().toString().trim();

// Extract and check the `arguments` field
if (fieldName.equals("arguments")) {
ExpressionNode valueExpr = specificField.valueExpr().orElse(null);
if (valueExpr instanceof ListConstructorExpressionNode listNode) {
for (Node listItem : listNode.expressions()) {
if (isUserControlledInput(listItem, context)) {
return true;
}
}
} else if (valueExpr instanceof SimpleNameReferenceNode refNode) {
// Check if the variable is assigned user-controlled input
if (isUserControlledInput(refNode, context)) {
return true;
}
}
}
}
}
}
}
return false;
Expand All @@ -88,34 +147,84 @@ private boolean isUserControlledInput(Node node, SyntaxNodeAnalysisContext conte
}

if (symbol.kind() == SymbolKind.VARIABLE) {
return isDerivedFromParameter(node);
return isAssignedUserControlledInput(node, context);
}

return false;
}

private boolean isInsidePublicFunction(Node node) {
Node parent = node.parent();
Node parent = node;
while (parent != null) {
if (parent instanceof FunctionDefinitionNode functionNode) {
return functionNode.qualifierList().stream()
.anyMatch(q -> q.text().equals("public"));
.anyMatch(q -> q.text().equals(PUBLIC_QUALIFIER));
}
parent = parent.parent();
parent = node.parent();
}
return false;
}

private boolean isDerivedFromParameter(Node node) {
private boolean isAssignedUserControlledInput(Node node, SyntaxNodeAnalysisContext context) {
Node parent = node.parent();

// Traverse up the AST to find where the variable is assigned
while (parent != null) {
if (parent instanceof FunctionDefinitionNode functionNode && isInsidePublicFunction(functionNode)) {
return functionNode.functionSignature().parameters().stream()
.anyMatch(param -> param.toSourceCode().equals(node.toSourceCode()));
// Check if this variable is assigned from a function parameter
for (var param : functionNode.functionSignature().parameters()) {
String paramName = ((RequiredParameterNode) param).paramName().get().text();

// Check if this variable (node) is assigned from a function parameter
if (isVariableAssignedFrom(node, paramName, functionNode, context)) {
return true;
}
}
}
parent = parent.parent();
}
return false;
}

private boolean isVariableAssignedFrom(Node variable, String paramName,
FunctionDefinitionNode functionNode, SyntaxNodeAnalysisContext context) {
FunctionBodyNode body = functionNode.functionBody();

if (body instanceof FunctionBodyBlockNode blockBody) {
for (var statement : blockBody.statements()) {
if (statement instanceof VariableDeclarationNode varDecl) {
if (varDecl.initializer().isPresent()) {
ExpressionNode initializer = varDecl.initializer().get();
if (initializer instanceof ListConstructorExpressionNode listExpr) {
for (var listItem : listExpr.expressions()) {
if (listItem.toSourceCode().equals(paramName)) {
return true;
}
}
} else if (initializer.toSourceCode().equals(paramName)) {
return true;
}
}
} else if (statement instanceof AssignmentStatementNode assignment) {
String assignedVar = assignment.varRef().toSourceCode();
ExpressionNode assignedValue = assignment.expression();

if (assignedVar.equals(variable.toSourceCode())) {
if (assignedValue.toSourceCode().equals(paramName)) {
return true;
}

if (assignedValue instanceof ListConstructorExpressionNode listExpr) {
for (var listItem : listExpr.expressions()) {
if (listItem.toSourceCode().equals(paramName)) {
return true;
}
}
}
}
}
}
}
return false;
}
}

0 comments on commit c9cc375

Please sign in to comment.