Including security review as a submodule - with patched for Yaffs.
[yaffs-website] / web / modules / contrib / security_review / src / Checks / QueryErrors.php
1 <?php
2
3 /**
4  * @file
5  * Contains \Drupal\security_review\Checks\QueryErrors.
6  */
7
8 namespace Drupal\security_review\Checks;
9
10 use Drupal\Core\Logger\RfcLogLevel;
11 use Drupal\security_review\Check;
12 use Drupal\security_review\CheckResult;
13
14 /**
15  * Checks for abundant query errors.
16  */
17 class QueryErrors extends Check {
18
19   /**
20    * {@inheritdoc}
21    */
22   public function getNamespace() {
23     return 'Security Review';
24   }
25
26   /**
27    * {@inheritdoc}
28    */
29   public function getTitle() {
30     return 'Query errors';
31   }
32
33   /**
34    * {@inheritdoc}
35    */
36   public function run() {
37     // If dblog is not enabled return with hidden INFO.
38     if (!$this->moduleHandler()->moduleExists('dblog')) {
39       return $this->createResult(CheckResult::INFO, [], FALSE);
40     }
41
42     $result = CheckResult::SUCCESS;
43     $findings = [];
44     $last_result = $this->lastResult();
45     $visible = FALSE;
46
47     // Prepare the query.
48     $query = $this->database()->select('watchdog', 'w');
49     $query->fields('w', [
50       'severity',
51       'type',
52       'timestamp',
53       'message',
54       'variables',
55       'hostname',
56     ]);
57     $query->condition('type', 'php')->condition('severity', RfcLogLevel::ERROR);
58     if ($last_result instanceof CheckResult) {
59       // Only check entries that got recorded since the last run of the check.
60       $query->condition('timestamp', $last_result->time(), '>=');
61     }
62
63     // Execute the query.
64     $db_result = $query->execute();
65
66     // Count the number of query errors per IP.
67     $entries = [];
68     foreach ($db_result as $row) {
69       // Get the message.
70       if ($row->variables === 'N;') {
71         $message = $row->message;
72       }
73       else {
74         $message = $this->t($row->message, unserialize($row->variables));
75       }
76
77       // Get the IP.
78       $ip = $row->hostname;
79
80       // Search for query errors.
81       $message_contains_sql = strpos($message, 'SQL') !== FALSE;
82       $message_contains_select = strpos($message, 'SELECT') !== FALSE;
83       if ($message_contains_sql && $message_contains_select) {
84         $entry_for_ip = &$entries[$ip];
85
86         if (!isset($entry_for_ip)) {
87           $entry_for_ip = 0;
88         }
89         $entry_for_ip++;
90       }
91     }
92
93     // Filter the IPs with more than 10 query errors.
94     if (!empty($entries)) {
95       foreach ($entries as $ip => $count) {
96         if ($count > 10) {
97           $findings[] = $ip;
98         }
99       }
100     }
101
102     if (!empty($findings)) {
103       $result = CheckResult::FAIL;
104       $visible = TRUE;
105     }
106
107     return $this->createResult($result, $findings, $visible);
108   }
109
110   /**
111    * {@inheritdoc}
112    */
113   public function help() {
114     $paragraphs = [];
115     $paragraphs[] = $this->t('Database errors triggered from the same IP may be an artifact of a malicious user attempting to probe the system for weaknesses like SQL injection or information disclosure.');
116
117     return [
118       '#theme' => 'check_help',
119       '#title' => $this->t('Abundant query errors from the same IP'),
120       '#paragraphs' => $paragraphs,
121     ];
122   }
123
124   /**
125    * {@inheritdoc}
126    */
127   public function evaluate(CheckResult $result) {
128     $findings = $result->findings();
129     if (empty($findings)) {
130       return [];
131     }
132
133     $paragraphs = [];
134     $paragraphs[] = $this->t('The following IPs were observed with an abundance of query errors.');
135
136     return [
137       '#theme' => 'check_evaluation',
138       '#paragraphs' => $paragraphs,
139       '#items' => $result->findings(),
140     ];
141   }
142
143   /**
144    * {@inheritdoc}
145    */
146   public function evaluatePlain(CheckResult $result) {
147     $findings = $result->findings();
148     if (empty($findings)) {
149       return '';
150     }
151
152     $output = $this->t('Suspicious IP addresses:') . ":\n";
153     foreach ($findings as $ip) {
154       $output .= "\t" . $ip . "\n";
155     }
156
157     return $output;
158   }
159
160   /**
161    * {@inheritdoc}
162    */
163   public function getMessage($result_const) {
164     switch ($result_const) {
165       case CheckResult::FAIL:
166         return $this->t('Query errors from the same IP. These may be a SQL injection attack or an attempt at information disclosure.');
167
168       default:
169         return $this->t('Unexpected result.');
170     }
171   }
172
173 }