Issues (10)

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/WebSocket/FrameHandler.php (2 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
3
namespace Helix\Socket\WebSocket;
4
5
use LogicException;
6
7
/**
8
 * Interprets parsed frames from the peer, and packs and writes frames.
9
 *
10
 * TODO: Multiplex by RSV.
11
 *
12
 * TODO: Stream writing.
13
 */
14
class FrameHandler {
15
16
    /**
17
     * The message buffer for data frames.
18
     *
19
     * @var string
20
     */
21
    protected $buffer = '';
22
23
    /**
24
     * @var WebSocketClient
25
     */
26
    protected $client;
27
28
    /**
29
     * Resume opCode for the `CONTINUATION` handler.
30
     *
31
     * @var int|null
32
     */
33
    protected $continue;
34
35
    /**
36
     * Max outgoing fragment size.
37
     *
38
     * Each browser has its own standard, so this is generalized.
39
     *
40
     * Defaults to 128 KiB.
41
     *
42
     * @var int
43
     */
44
    protected $fragmentSize = 128 * 1024;
45
46
    /**
47
     * Maximum inbound message length (complete payload).
48
     *
49
     * Defaults to 10 MiB.
50
     *
51
     * @var int
52
     */
53
    protected $maxLength = 10 * 1024 * 1024;
54
55
    /**
56
     * @var FrameReader
57
     */
58
    protected $reader;
59
60
    /**
61
     * Whether binary I/O should bypass buffers.
62
     *
63
     * @var bool
64
     */
65
    protected $stream = false;
66
67
    /**
68
     * @param WebSocketClient $client
69
     */
70
    public function __construct (WebSocketClient $client) {
71
        $this->client = $client;
72
        $this->reader = new FrameReader($client);
73
    }
74
75
    /**
76
     * @return int
77
     */
78
    public function getFragmentSize (): int {
79
        return $this->fragmentSize;
80
    }
81
82
    /**
83
     * @return int
84
     */
85
    public function getMaxLength (): int {
86
        return $this->maxLength;
87
    }
88
89
    /**
90
     * @return bool
91
     */
92
    public function isStream (): bool {
93
        return $this->stream;
94
    }
95
96
    /**
97
     * Progressively receives `BINARY` data into the buffer until the payload is complete.
98
     * Passes the complete payload up to {@link WebSocketClient::onBinary()}
99
     *
100
     * When {@link $stream} is `true`, this bypasses the buffer.
101
     *
102
     * @param Frame $frame
103
     * @throws WebSocketError
104
     */
105
    protected function onBinary (Frame $frame): void {
106
        if ($this->stream) {
107
            $this->client->onBinary($frame->getPayload());
108
        }
109
        else {
110
            $this->onData_CheckLength($frame);
111
            $this->buffer .= $frame->getPayload();
112
            if ($frame->isFinal()) {
113
                $binary = $this->buffer;
114
                $this->buffer = '';
115
                $this->client->onBinary($binary);
116
            }
117
        }
118
    }
119
120
    /**
121
     * When a `CLOSE` frame is received. Calls {@link WebSocketClient::onClose()}
122
     *
123
     * https://tools.ietf.org/html/rfc6455#section-5.5.1
124
     * > If an endpoint receives a Close frame and did not previously send a
125
     * > Close frame, the endpoint MUST send a Close frame in response.  (When
126
     * > sending a Close frame in response, the endpoint typically echos the
127
     * > status code it received.)
128
     *
129
     * @param Frame $frame
130
     */
131
    protected function onClose (Frame $frame): void {
132
        $this->client->onClose($frame->getCloseCode(), $frame->getCloseReason());
133
    }
134
135
    /**
136
     * When a `CONTINUATION` frame (data fragment) is received.
137
     *
138
     * @param Frame $frame
139
     * @throws WebSocketError
140
     */
141
    protected function onContinuation (Frame $frame): void {
142
        if (!$this->continue) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->continue of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
143
            throw new WebSocketError(
144
                Frame::CLOSE_PROTOCOL_ERROR,
145
                "Received CONTINUATION without a prior fragment.",
146
                $frame
147
            );
148
        }
149
        try {
150
            if ($this->continue === Frame::OP_TEXT) {
151
                $this->onText($frame);
152
            }
153
            else {
154
                $this->onBinary($frame);
155
            }
156
        }
157
        finally {
158
            if ($frame->isFinal()) {
159
                $this->continue = null;
160
            }
161
        }
162
    }
163
164
    /**
165
     * When a control frame is received.
166
     *
167
     * https://tools.ietf.org/html/rfc6455#section-5.4
168
     * > Control frames (see Section 5.5) MAY be injected in the middle of
169
     * > a fragmented message.
170
     *
171
     * @param Frame $frame
172
     */
173
    protected function onControl (Frame $frame): void {
174
        if ($frame->isClose()) {
175
            $this->onClose($frame);
176
        }
177
        elseif ($frame->isPing()) {
178
            $this->onPing($frame);
179
        }
180
        elseif ($frame->isPong()) {
181
            $this->onPong($frame);
182
        }
183
        else {
184
            throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, "Unsupported control frame.", $frame);
185
        }
186
    }
187
188
    /**
189
     * When an initial data frame (not `CONTINUATION`) is received.
190
     *
191
     * @param Frame $frame
192
     */
193
    protected function onData (Frame $frame): void {
194
        // did we get a continuation?
195
        if ($frame->isContinuation()) {
196
            $this->onContinuation($frame);
197
        }
198
        // were we expecting one?
199
        elseif ($this->continue) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->continue of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
200
            throw new WebSocketError(
201
                Frame::CLOSE_PROTOCOL_ERROR,
202
                "Received interleaved {$frame->getName()} against existing " . Frame::NAMES[$this->continue],
203
                $frame
204
            );
205
        }
206
        // the data is new
207
        else {
208
            // will we get a continuation later?
209
            if (!$frame->isFinal()) {
210
                $this->continue = $frame->getOpCode();
211
            }
212
            // handle new text
213
            if ($frame->isText()) {
214
                $this->onText($frame);
215
            }
216
            // handle new binary
217
            else {
218
                $this->onBinary($frame);
219
            }
220
        }
221
    }
222
223
    /**
224
     * Validates the message length, but only when the buffer is in use.
225
     *
226
     * @param Frame $frame
227
     * @throws WebSocketError
228
     */
229
    protected function onData_CheckLength (Frame $frame): void {
230
        if (strlen($this->buffer) + $frame->getLength() > $this->maxLength) {
231
            throw new WebSocketError(
232
                Frame::CLOSE_TOO_LARGE,
233
                "Message would exceed {$this->maxLength} bytes",
234
                $frame
235
            );
236
        }
237
    }
238
239
    /**
240
     * Called by {@link WebSocketClient} when a complete frame has been received.
241
     *
242
     * Delegates to the other handler methods using the program logic outlined in the RFC.
243
     *
244
     * Eventually calls back to the {@link WebSocketClient} when payloads are complete.
245
     *
246
     * @param Frame $frame
247
     */
248
    public function onFrame (Frame $frame): void {
249
        if ($frame->isControl()) {
250
            $this->onControl($frame);
251
        }
252
        else {
253
            $this->onData($frame);
254
        }
255
    }
256
257
    /**
258
     * When a `PING` is received. Calls {@link WebSocketClient::onPing()}
259
     *
260
     * @param Frame $frame
261
     */
262
    protected function onPing (Frame $frame): void {
263
        $this->client->onPing($frame->getPayload());
264
    }
265
266
    /**
267
     * When a `PONG` is received. Calls {@link WebSocketClient::onPong()}
268
     *
269
     * @param Frame $frame
270
     */
271
    protected function onPong (Frame $frame): void {
272
        $this->client->onPong($frame->getPayload());
273
    }
274
275
    /**
276
     * Uses {@link FrameReader} to read frames and passes them off to {@link onFrame()}
277
     */
278
    public function onReadable (): void {
279
        foreach ($this->reader->getFrames() as $frame) {
280
            $this->onFrame($frame);
281
        }
282
    }
283
284
    /**
285
     * Progressively receives `TEXT` data until the payload is complete.
286
     * Validates the complete payload as UTF-8 and passes it up to {@link WebSocketClient::onText()}
287
     *
288
     * @param Frame $frame
289
     * @throws WebSocketError
290
     */
291
    protected function onText (Frame $frame): void {
292
        $this->onData_CheckLength($frame);
293
        $this->buffer .= $frame->getPayload();
294
        if ($frame->isFinal()) {
295
            if (!mb_detect_encoding($this->buffer, 'UTF-8', true)) {
296
                throw new WebSocketError(Frame::CLOSE_BAD_DATA, "The received TEXT is not UTF-8.");
297
            }
298
            $text = $this->buffer;
299
            $this->buffer = '';
300
            $this->client->onText($text);
301
        }
302
    }
303
304
    /**
305
     * @param int $bytes
306
     * @return $this
307
     */
308
    public function setFragmentSize (int $bytes) {
309
        $this->fragmentSize = $bytes;
310
        return $this;
311
    }
312
313
    /**
314
     * @param int $bytes
315
     * @return $this
316
     */
317
    public function setMaxLength (int $bytes) {
318
        $this->maxLength = $bytes;
319
        return $this;
320
    }
321
322
    /**
323
     * @param bool $stream
324
     * @return $this
325
     */
326
    public function setStream (bool $stream) {
327
        $this->stream = $stream;
328
        return $this;
329
    }
330
331
    /**
332
     * Sends a complete message to the peer, fragmenting if needed.
333
     *
334
     * @param int $opCode
335
     * @param string $payload
336
     */
337
    public function write (int $opCode, string $payload): void {
338
        $offset = 0;
339
        $total = strlen($payload);
340
        do {
341
            $fragment = substr($payload, $offset, $this->fragmentSize);
342
            if ($offset) {
343
                $opCode = Frame::OP_CONTINUATION;
344
            }
345
            $offset += strlen($fragment);
346
            $this->writeFrame($offset >= $total, $opCode, $fragment);
347
        } while ($offset < $total);
348
    }
349
350
    /**
351
     * @param string $payload
352
     */
353
    public function writeBinary (string $payload): void {
354
        $this->write(Frame::OP_BINARY, $payload);
355
    }
356
357
    /**
358
     * @param int $code
359
     * @param string $reason
360
     */
361
    public function writeClose (int $code = Frame::CLOSE_NORMAL, string $reason = ''): void {
362
        $this->writeFrame(true, Frame::OP_CLOSE, pack('n', $code) . $reason);
363
    }
364
365
    /**
366
     * Writes a single frame.
367
     *
368
     * @param bool $final
369
     * @param int $opCode
370
     * @param string $payload
371
     */
372
    protected function writeFrame (bool $final, int $opCode, string $payload): void {
373
        if ($opCode & 0x08 and !$final) {
374
            throw new LogicException("Would have sent a fragmented control frame ({$opCode}) {$payload}");
375
        }
376
        $head = chr($final ? 0x80 | $opCode : $opCode);
377
        $length = strlen($payload);
378
        if ($length > 65535) {
379
            $head .= chr(127);
380
            $head .= pack('J', $length);
381
        }
382
        elseif ($length >= 126) {
383
            $head .= chr(126);
384
            $head .= pack('n', $length);
385
        }
386
        else {
387
            $head .= chr($length);
388
        }
389
        $this->client->write($head . $payload);
390
    }
391
392
    /**
393
     * @param string $payload
394
     */
395
    public function writePing (string $payload = ''): void {
396
        $this->writeFrame(true, Frame::OP_PING, $payload);
397
    }
398
399
    /**
400
     * @param string $payload
401
     */
402
    public function writePong (string $payload = ''): void {
403
        $this->writeFrame(true, Frame::OP_PONG, $payload);
404
    }
405
406
    /**
407
     * @param string $payload
408
     */
409
    public function writeText (string $payload): void {
410
        $this->write(Frame::OP_TEXT, $payload);
411
    }
412
}