SqlInjectionCheckerPlugin.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
  4. *
  5. * @license GNU AGPL version 3 or any later version
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Affero General Public License as
  9. * published by the Free Software Foundation, either version 3 of the
  10. * License, or (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Affero General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. *
  20. */
  21. declare(strict_types=1);
  22. use Phan\PluginV2;
  23. use Phan\PluginV2\AnalyzeNodeCapability;
  24. use Phan\PluginV2\PluginAwareAnalysisVisitor;
  25. class SqlInjectionCheckerPlugin extends PluginV2 implements AnalyzeNodeCapability{
  26. public static function getAnalyzeNodeVisitorClassName() : string {
  27. return SqlInjectionCheckerVisitor::class;
  28. }
  29. }
  30. class SqlInjectionCheckerVisitor extends PluginAwareAnalysisVisitor {
  31. private function throwError(string $hint) {
  32. $this->emit(
  33. 'SqlInjectionChecker',
  34. 'Potential SQL injection detected - ' . $hint,
  35. [],
  36. \Phan\Issue::SEVERITY_CRITICAL
  37. );
  38. }
  39. /**
  40. * Checks whether the query builder functions are using prepared statements
  41. *
  42. * @param \ast\Node $node
  43. */
  44. private function checkQueryBuilderParameters(\ast\Node $node) {
  45. $dangerousFunctions = [
  46. 'eq',
  47. 'neq',
  48. 'lt',
  49. 'lte',
  50. 'gt',
  51. 'gte',
  52. 'like',
  53. 'iLike',
  54. 'notLike',
  55. ];
  56. $safeFunctions = [
  57. 'createNamedParameter',
  58. 'createPositionalParameter',
  59. 'createParameter',
  60. 'createFunction',
  61. 'func',
  62. ];
  63. $functionsToSearch = [
  64. 'set',
  65. 'setValue',
  66. ];
  67. $expandedNode = \Phan\Language\UnionType::fromNode($this->context, $this->code_base, $node);
  68. $expandedNodeType = (string)$expandedNode->asExpandedTypes($this->code_base);
  69. if($expandedNodeType === '\OCP\DB\QueryBuilder\IQueryBuilder') {
  70. /** @var \ast\Node $child */
  71. foreach($node->children as $child) {
  72. if(isset($child->kind) && $child->kind === 128) {
  73. if(isset($child->children)) {
  74. /** @var \ast\Node $subChild */
  75. foreach ($child->children as $subChild) {
  76. // For set actions
  77. if(isset($node->children['method']) && in_array($node->children['method'], $functionsToSearch, true) && !is_string($subChild)) {
  78. if(!isset($subChild->children['method']) || !in_array($subChild->children['method'], $safeFunctions, true)) {
  79. $this->throwError('method: ' . ($subChild->children['method'] ?? 'no child method'));
  80. }
  81. }
  82. if(isset($subChild->children['method'])) {
  83. // For all "eq" etc. actions
  84. $method = $subChild->children['method'];
  85. if(!in_array($method, $dangerousFunctions, true)) {
  86. return;
  87. }
  88. /** @var \ast\Node $functionNode */
  89. $functionNode = $subChild->children['args'];
  90. /** @var \ast\Node $secondParameterNode */
  91. $secondParameterNode = $functionNode->children[1];
  92. $expandedNode = \Phan\Language\UnionType::fromNode($this->context, $this->code_base, $secondParameterNode);
  93. // For literals with a plain string or integer inside
  94. if(isset($secondParameterNode->children['method']) && $secondParameterNode->children['method'] === 'literal') {
  95. /** @var \ast\Node $functionNode */
  96. $functionNode = $secondParameterNode->children['args'];
  97. $expandedNode = \Phan\Language\UnionType::fromNode($this->context, $this->code_base, $functionNode);
  98. if(isset($functionNode->children[0]) && (is_string($functionNode->children[0]) || is_int($functionNode->children[0]))) {
  99. return;
  100. }
  101. }
  102. // If it is an IParameter or a pure string no error is thrown
  103. if((string)$expandedNode !== '\OCP\DB\QueryBuilder\IParameter' && !is_string($secondParameterNode)) {
  104. $this->throwError('neither a parameter nor a string');
  105. }
  106. }
  107. }
  108. }
  109. }
  110. }
  111. }
  112. }
  113. public function visitMethodCall(\ast\Node $node) {
  114. $this->checkQueryBuilderParameters($node);
  115. }
  116. }
  117. return new SqlInjectionCheckerPlugin();