All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.php.checks.security.XxeCheck Maven / Gradle / Ivy

The newest version!
/*
 * SonarQube PHP Plugin
 * Copyright (C) 2010-2024 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonar.php.checks.security;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.sonar.check.Rule;
import org.sonar.php.checks.utils.CheckUtils;
import org.sonar.php.tree.TreeUtils;
import org.sonar.php.tree.impl.declaration.ClassNamespaceNameTreeImpl;
import org.sonar.php.tree.impl.expression.MemberAccessTreeImpl;
import org.sonar.plugins.php.api.tree.Tree;
import org.sonar.plugins.php.api.tree.declaration.CallArgumentTree;
import org.sonar.plugins.php.api.tree.declaration.NamespaceNameTree;
import org.sonar.plugins.php.api.tree.expression.ArrayInitializerTree;
import org.sonar.plugins.php.api.tree.expression.BinaryExpressionTree;
import org.sonar.plugins.php.api.tree.expression.ExpressionTree;
import org.sonar.plugins.php.api.tree.expression.FunctionCallTree;
import org.sonar.plugins.php.api.tree.expression.ParenthesisedExpressionTree;
import org.sonar.plugins.php.api.tree.expression.VariableIdentifierTree;
import org.sonar.plugins.php.api.tree.statement.NamespaceStatementTree;
import org.sonar.plugins.php.api.visitors.PHPVisitorCheck;
import org.sonar.plugins.php.api.visitors.PreciseIssue;

import static org.sonar.php.checks.utils.CheckUtils.argument;
import static org.sonar.php.checks.utils.CheckUtils.arrayValue;
import static org.sonar.php.checks.utils.CheckUtils.isFalseValue;
import static org.sonar.php.checks.utils.CheckUtils.isTrueValue;
import static org.sonar.php.checks.utils.CheckUtils.nameOf;
import static org.sonar.plugins.php.api.tree.Tree.Kind;

@Rule(key = "S2755")
public class XxeCheck extends PHPVisitorCheck {

  private static final String MESSAGE = "Disable access to external entities in XML parsing.";
  private static final String SECONDARY_MESSAGE = "This value enables external entities in XML parsing.";
  private static final String PROPAGATED_MESSAGE = "Propagated settings.";

  private static final String OPTIONS = "options";
  private static final Tree.Kind[] ARRAY = {Tree.Kind.ARRAY_INITIALIZER_BRACKET, Tree.Kind.ARRAY_INITIALIZER_FUNCTION};

  @Override
  public void visitFunctionCall(FunctionCallTree call) {
    String functionName = CheckUtils.lowerCaseFunctionName(call);
    ExpressionTree callee = call.callee();
    if (callee.is(Kind.NAMESPACE_NAME) && "simplexml_load_string".equals(functionName)) {
      argument(call, OPTIONS, 2).ifPresent(options -> checkSimpleXmlOption(options.value(), options));
    } else if (callee.is(Kind.OBJECT_MEMBER_ACCESS)) {
      if ("load".equals(functionName) || "loadxml".equals(functionName)) {
        argument(call, OPTIONS, 1).ifPresent(options -> checkSimpleXmlOption(options.value(), options));
      } else if ("setparserproperty".equals(functionName)) {
        checkSetParserProperty(call);
      }
    } else if (callee.is(Kind.CLASS_MEMBER_ACCESS) && "build".equals(functionName) &&
      isNamespaceMemberEqualTo("Cake\\Utility\\Xml", callee)) {
        argument(call, OPTIONS, 1).ifPresent(options -> checkXmlBuildOption(options.value(), options));
      }
    super.visitFunctionCall(call);
  }

  private void checkSimpleXmlOption(ExpressionTree optionValue, Tree treeToReport) {
    if (optionValue.is(Kind.NAMESPACE_NAME) && "LIBXML_NOENT".equals(((NamespaceNameTree) optionValue).unqualifiedName())) {
      createIssue(treeToReport);
    } else if (optionValue.is(Kind.BITWISE_OR)) {
      BinaryExpressionTree orExpression = (BinaryExpressionTree) optionValue;
      checkSimpleXmlOption(orExpression.leftOperand(), treeToReport);
      checkSimpleXmlOption(orExpression.rightOperand(), treeToReport);
    } else if (optionValue.is(Kind.PARENTHESISED_EXPRESSION)) {
      checkSimpleXmlOption(((ParenthesisedExpressionTree) optionValue).expression(), treeToReport);
    } else if (optionValue.is(Kind.VARIABLE_IDENTIFIER)) {
      CheckUtils.uniqueAssignedValue((VariableIdentifierTree) optionValue).ifPresent(x -> checkSimpleXmlOption(x, treeToReport));
    }
  }

  private void checkSetParserProperty(FunctionCallTree call) {
    Optional property = argument(call, "property", 0);
    if (property.isPresent() && "XMLReader::SUBST_ENTITIES".equalsIgnoreCase(nameOf(property.get().value()))) {
      Optional value = argument(call, "value", 1);
      if (value.isPresent() && isTrueValue(value.get().value())) {
        createIssue(call);
      }
    }
  }

  private void checkXmlBuildOption(ExpressionTree optionValue, Tree optionArgument) {
    if (optionValue.is(Kind.VARIABLE_IDENTIFIER)) {
      CheckUtils.uniqueAssignedValue((VariableIdentifierTree) optionValue)
        .filter(uniqueValue -> !uniqueValue.toString().equals(optionArgument.toString()))
        .ifPresent(assignedValue -> checkXmlBuildOption(assignedValue, optionArgument));
    } else if (optionValue.is(ARRAY)) {
      arrayValue((ArrayInitializerTree) optionValue, "loadEntities")
        .ifPresent(loadEntitiesValue -> raiseIssueIfTrue(loadEntitiesValue, optionArgument));
    }
  }

  private void raiseIssueIfTrue(ExpressionTree value, Tree treeToReport) {
    ExpressionTree assignedValue = CheckUtils.assignedValue(value);
    List secondaryTrees = new ArrayList<>();
    while (assignedValue.is(Kind.VARIABLE_IDENTIFIER)) {
      secondaryTrees.add(assignedValue);
      assignedValue = CheckUtils.assignedValue(assignedValue);
    }
    if (!isFalseValue(assignedValue)) {
      PreciseIssue issue = createIssue(treeToReport);
      issue.secondary(assignedValue, SECONDARY_MESSAGE);
      secondaryTrees.forEach(tree -> issue.secondary(tree, PROPAGATED_MESSAGE));
    }
  }

  private PreciseIssue createIssue(Tree tree) {
    return context().newIssue(this, tree, MESSAGE);
  }

  private static Optional namespaceMemberFullQualifiedName(ExpressionTree callee) {
    return Optional.of(callee)
      .filter(MemberAccessTreeImpl.class::isInstance)
      .map(c -> ((MemberAccessTreeImpl) c).object())
      .filter(ClassNamespaceNameTreeImpl.class::isInstance)
      .map(ClassNamespaceNameTreeImpl.class::cast)
      .map(c -> c.symbol().qualifiedName().toString());
  }

  private static boolean isNamespaceMemberEqualTo(String targetNamespace, ExpressionTree callee) {
    return namespaceMemberFullQualifiedName(callee)
      .filter(name -> targetNamespace.equalsIgnoreCase(name) || isNameInNamespaceEqualTo(targetNamespace, callee, name))
      .isPresent();
  }

  private static boolean isNameInNamespaceEqualTo(String targetNamespace, ExpressionTree callee, String name) {
    return Optional.ofNullable((NamespaceStatementTree) TreeUtils.findAncestorWithKind(callee, Collections.singletonList(Kind.NAMESPACE_STATEMENT)))
      .map(NamespaceStatementTree::namespaceName)
      .filter(nsName -> (nsName.fullName() + "\\" + targetNamespace).equalsIgnoreCase(name))
      .isPresent();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy