Issues (22)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Connection.php (7 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
namespace SamIT\React\Smtp;
3
4
5
use React\Dns\Query\TimeoutException;
6
use React\EventLoop\LoopInterface;
7
use React\EventLoop\Timer\TimerInterface;
8
use React\Socket\ConnectionInterface;
9
use React\Stream\WritableStream;
10
11
class Connection extends \React\Socket\Connection{
12
    const STATUS_NEW = 0;
13
    const STATUS_INIT = 1;
14
    const STATUS_FROM = 2;
15
    const STATUS_TO = 3;
16
    const STATUS_HEADERS = 4;
17
    const STATUS_UNFOLDING = 5;
18
    const STATUS_BODY = 6;
19
20
21
    /**
22
     * This status is used when all mail data has been received and the system is deciding whether to accept or reject.
23
     */
24
    const STATUS_PROCESSING = 7;
25
26
27
    const REGEXES = [
28
        'Quit' => '/^QUIT$/',
29
        'Helo' => '/^HELO (.*)$/',
30
        'Ehlo' => '/^EHLO (.*)$/',
31
        'MailFrom' => '/^MAIL FROM:\s*(.*)$/',
32
        'Reset' => '/^RSET$/',
33
        'RcptTo' => '/^RCPT TO:\s*(.*)$/',
34
        'StartData' => '/^DATA$/',
35
        'StartHeader' => '/^(\w+):\s*(.*)$/',
36
        'StartBody' => '/^$/',
37
        'Unfold' => '/^ (.*)$/',
38
        'EndData' => '/^\.$/',
39
        'BodyLine' => '/^(.*)$/',
40
        'EndBody' => '/^\.$/'
41
    ];
42
43
    protected $states = [
44
        self::STATUS_NEW => [
45
            'Quit', 'Helo', 'Ehlo'
46
        ],
47
        self::STATUS_INIT => [
48
            'MailFrom',
49
            'Quit'
50
51
        ],
52
        self::STATUS_FROM => [
53
            'RcptTo',
54
            'Quit',
55
            'Reset',
56
        ],
57
        self::STATUS_TO => [
58
            'Quit',
59
            'StartData',
60
            'Reset',
61
            'RcptTo',
62
63
        ],
64
        self::STATUS_HEADERS => [
65
            'EndBody',
66
            'StartHeader',
67
            'StartBody',
68
        ],
69
        self::STATUS_UNFOLDING => [
70
            'StartBody',
71
            'EndBody',
72
            'Unfold',
73
            'StartHeader',
74
        ],
75
        self::STATUS_BODY => [
76
            'EndBody',
77
            'BodyLine'
78
        ],
79
        self::STATUS_PROCESSING => [
80
81
        ]
82
83
84
85
    ];
86
87
    protected $state = self::STATUS_NEW;
88
89
    protected $banner = 'Welcome to ReactPHP SMTP Server';
90
    /**
91
     * @var bool Accept messages by default
92
     */
93
    protected $acceptByDefault = true;
94
    /**
95
     * If there are event listeners, how long will they get to accept or reject a message?
96
     * @var int
97
     */
98
    protected $defaultActionTimeout = 5;
99
    /**
100
     * The timer for the default action, canceled in [accept] and [reject]
101
     * @var TimerInterface
102
     */
103
    protected $defaultActionTimer;
104
    /**
105
     * The current line buffer used by handleData.
106
     * @var string
107
     */
108
    protected $lineBuffer = '';
109
110
    /**
111
     * @var string Name of the header in the foldBuffer.
112
     */
113
    protected $foldHeader = '';
114
    /**
115
     * Buffer used for unfolding multiline headers..
116
     * @var string
117
     */
118
    protected $foldBuffer = '';
119
    protected $from;
120
    protected $recipients = [];
121
    /**
122
     * @var Message
123
     */
124
    protected $message;
125
126
    public $bannerDelay = 0;
127
128
129
    public $recipientLimit = 100;
130
131 1
    public function __construct($stream, LoopInterface $loop)
132
    {
133 1
        parent::__construct($stream, $loop);
134 1
        stream_get_meta_data($stream);
135
        // We sleep for 3 seconds, if client does not wait for our banner we disconnect.
136
        $disconnect = function($data, ConnectionInterface $conn) {
137
            $conn->end("I can break rules too, bye.\n");
138 1
        };
139 1
        $this->on('data', $disconnect);
140 1
        $this->reset(self::STATUS_NEW);
141 1
        $this->on('line', [$this, 'handleCommand']);
142 1
        if ($this->bannerDelay > 0) {
143
            $loop->addTimer($this->bannerDelay, function () use ($disconnect) {
144
                $this->sendReply(220, $this->banner);
145
                $this->removeListener('data', $disconnect);
146
            });
147
        } else {
148 1
            $this->sendReply(220, $this->banner);
149
        }
150 1
    }
151
152
    /**
153
     * We read until we find an and of line sequence for SMTP.
154
     * http://www.jebriggs.com/blog/2010/07/smtp-maximum-line-lengths/
155
     * @param $stream
156
     */
157
    public function handleData($stream)
158
    {
159
        // Socket is raw, not using fread as it's interceptable by filters
160
        // See issues #192, #209, and #240
161
        $data = stream_socket_recvfrom($stream, $this->bufferSize);;
162
163
        $limit = $this->state == self::STATUS_BODY ? 1000 : 512;
164
        if ('' !== $data && false !== $data) {
165
            $this->lineBuffer .= $data;
166
            if (strlen($this->lineBuffer) > $limit) {
167
                $this->sendReply(500, "Line length limit exceeded.");
168
                $this->lineBuffer = '';
169
            }
170
171
            $delimiter = "\r\n";
172
            while(false !== $pos = strpos($this->lineBuffer, $delimiter)) {
173
                $line = substr($this->lineBuffer, 0, $pos);
174
                $this->lineBuffer = substr($this->lineBuffer, $pos + strlen($delimiter));
175
                $this->emit('line', [$line, $this]);
176
            }
177
        }
178
179
        if ('' === $data || false === $data || !is_resource($stream) || feof($stream)) {
180
            $this->end();
181
        }
182
    }
183
184
    /**
185
     * Parses the command from the beginning of the line.
186
     *
187
     * @param string $line
188
     * @return string[] An array containing the command and all arguments.
189
     */
190
    protected function parseCommand($line)
191
    {
192
193
        foreach ($this->states[$this->state] as $key) {
194
            if (preg_match(self::REGEXES[$key], $line, $matches) === 1) {
195
                $matches[0] = $key;
196
                $this->emit('debug', ["$line match for $key (" . self::REGEXES[$key] . ")"]);
197
                return $matches;
198
            } else {
199
                $this->emit('debug', ["$line does not match for $key (" . self::REGEXES[$key] . ")"]);
200
            }
201
        }
202
        return [null];
203
    }
204
205
    protected function handleCommand($line)
206
    {
207
        $arguments = $this->parseCommand($line);
208
        $command = array_shift($arguments);
209
        if ($command === null) {
210
            $this->sendReply(500, array_merge(
211
                $this->states[$this->state],
212
                ["Unexpected or unknown command."]
213
            ));
214
        } else {
215
            call_user_func_array([$this, "handle{$command}Command"], $arguments);
216
        }
217
    }
218
219 1
    protected function sendReply($code, $message, $close = false)
220
    {
221 1
        $out = '';
222 1
        if (is_array($message)) {
223
            $last = array_pop($message);
224
            foreach($message as $line) {
225
                $out .= "$code-$line\r\n";
226
            }
227
            $this->write($out);
228
            $message = $last;
229
        }
230 1
        if ($close) {
231
            $this->end("$code $message\r\n");
232
        } else {
233 1
            $this->write("$code $message\r\n");
234
        }
235
236 1
    }
237
238
    protected function handleResetCommand()
239
    {
240
        $this->reset();
241
        $this->sendReply(250, "Reset OK");
242
    }
243
    protected function handleHeloCommand($domain)
244
    {
245
        $this->state = self::STATUS_INIT;
246
        $this->sendReply(250, "Hello {$domain} @ {$this->getRemoteAddress()}");
247
    }
248
249
    protected function handleEhloCommand($domain)
250
    {
251
        $this->state = self::STATUS_INIT;
252
        $this->sendReply(250, "Hello {$domain} @ {$this->getRemoteAddress()}");
253
    }
254
255 View Code Duplication
    protected function handleMailFromCommand($arguments)
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
256
    {
257
258
        // Parse the email.
259
        if (preg_match('/\<(?<email>.*)\>( .*)?/', $arguments, $matches) == 1) {
260
            $this->state = self::STATUS_FROM;
261
            $this->from  = $matches['email'];
262
            $this->sendReply(250, "MAIL OK");
263
        } else {
264
            $this->sendReply(500, "Invalid mail argument");
265
        }
266
267
    }
268
269
    protected function handleQuitCommand()
270
    {
271
        $this->sendReply(221, "Goodbye.", true);
272
273
    }
274
275 View Code Duplication
    protected function handleRcptToCommand($arguments) {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
276
        // Parse the recipient.
277
        if (preg_match('/^(?<name>.*?)\s*?\<(?<email>.*)\>\s*$/', $arguments, $matches) == 1) {
278
            // Always set to 3, since this command might occur multiple times.
279
            $this->state = self::STATUS_TO;
280
            $this->recipients[$matches['email']] = $matches['name'];
281
            $this->sendReply(250, "Accepted");
282
        } else {
283
            $this->sendReply(500, "Invalid RCPT TO argument.");
284
        }
285
    }
286
287
    protected function handleStartDataCommand()
288
    {
289
        $this->state = self::STATUS_HEADERS;
290
        $this->sendReply(354, "Enter message, end with CRLF . CRLF");
291
    }
292
293
    protected function handleUnfoldCommand($content)
294
    {
295
        $this->foldBuffer .= $content;
296
    }
297
298
    protected function handleStartHeaderCommand($name, $content)
299
    {
300
        // Check if status is unfolding.
301 View Code Duplication
        if ($this->state === self::STATUS_UNFOLDING) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
302
            $this->message = $this->message->withAddedHeader($this->foldHeader, $this->foldBuffer);
303
        }
304
305
        $this->foldBuffer = $content;
306
        $this->foldHeader = $name;
307
        $this->state = self::STATUS_UNFOLDING;
308
    }
309
310
    protected function handleStartBodyCommand()
311
    {
312
        // Check if status is unfolding.
313 View Code Duplication
        if ($this->state === self::STATUS_UNFOLDING) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
314
            $this->message = $this->message->withAddedHeader($this->foldHeader, $this->foldBuffer);
315
        }
316
        $this->state = self::STATUS_BODY;
317
318
    }
319
320
    protected function handleEndBodyCommand()
321
    {
322
        // Check if status is unfolding.
323 View Code Duplication
        if ($this->state === self::STATUS_UNFOLDING) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
324
            $this->message = $this->message->withAddedHeader($this->foldHeader, $this->foldBuffer);
325
        }
326
327
        $this->state = self::STATUS_PROCESSING;
328
        /**
329
         * Default action, using timer so that callbacks above can be called asynchronously.
330
         */
331
        $this->defaultActionTimer = $this->loop->addTimer($this->defaultActionTimeout, function() {
332
            if ($this->acceptByDefault) {
333
                $this->accept();
334
            } else {
335
                $this->reject();
336
            }
337
        });
338
339
340
341
        $this->emit('message', [
342
            'from' => $this->from,
343
            'recipients' => $this->recipients,
344
            'message' => $this->message,
345
            'connection' => $this,
346
        ]);
347
    }
348
    protected function handleBodyLineCommand($line)
349
    {
350
        $this->message->getBody()->write($line);
351
    }
352
353
    /**
354
     * Reset the SMTP session.
355
     * By default goes to the initialized state (ie no new EHLO or HELO is required / possible.)
356
     *
357
     * @param int $state The state to go to.
358
     */
359 1
    protected function reset($state = self::STATUS_INIT) {
360 1
        $this->state = $state;
361 1
        $this->from = null;
362 1
        $this->recipients = [];
363 1
        $this->message = new Message();
364 1
    }
365
366 View Code Duplication
    public function accept($message = "OK") {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
367
        if ($this->state != self::STATUS_PROCESSING) {
368
            throw new \DomainException("SMTP Connection not in a valid state to accept a message.");
369
        }
370
        $this->loop->cancelTimer($this->defaultActionTimer);
371
        unset($this->defaultActionTimer);
372
        $this->sendReply(250, $message);
373
        $this->reset();
374
    }
375
376 View Code Duplication
    public function reject($code = 550, $message = "Message not accepted") {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
377
        if ($this->state != self::STATUS_PROCESSING) {
378
            throw new \DomainException("SMTP Connection not in a valid state to reject message.");
379
        }
380
        $this->defaultActionTimer->cancel();
381
        unset($this->defaultActionTimer);
382
        $this->sendReply($code, $message);
383
        $this->reset();
384
    }
385
386
    /**
387
     * Delay the default action by $seconds.
388
     * @param int $seconds
389
     */
390
    public function delay($seconds) {
391
        if (isset($this->defaultActionTimer)) {
392
            $this->defaultActionTimer->cancel();
393
            $this->defaultActionTimer = $this->loop->addTimer($seconds, $this->defaultActionTimer->getCallback());
394
        }
395
    }
396
397
}
398