diff --git a/src/Protocol/Parser.php b/src/Protocol/Parser.php index 5dba67a6..56cc9490 100644 --- a/src/Protocol/Parser.php +++ b/src/Protocol/Parser.php @@ -66,7 +66,7 @@ private function parse($data, Message $message) public function parseHeader(Message $message) { - if (strlen($message->data) < 12) { + if (!isset($message->data[12 - 1])) { return; } @@ -97,19 +97,11 @@ public function parseHeader(Message $message) public function parseQuestion(Message $message) { - if (strlen($message->data) < 2) { - return; - } - $consumed = $message->consumed; list($labels, $consumed) = $this->readLabels($message->data, $consumed); - if (null === $labels) { - return; - } - - if (strlen($message->data) - $consumed < 4) { + if ($labels === null || !isset($message->data[$consumed + 4 - 1])) { return; } @@ -133,19 +125,11 @@ public function parseQuestion(Message $message) public function parseAnswer(Message $message) { - if (strlen($message->data) < 2) { - return; - } - $consumed = $message->consumed; - list($labels, $consumed) = $this->readLabels($message->data, $consumed); + list($name, $consumed) = $this->readDomain($message->data, $consumed); - if (null === $labels) { - return; - } - - if (strlen($message->data) - $consumed < 10) { + if ($name === null || !isset($message->data[$consumed + 10 - 1])) { return; } @@ -163,68 +147,85 @@ public function parseAnswer(Message $message) list($rdLength) = array_values(unpack('n', substr($message->data, $consumed, 2))); $consumed += 2; - $rdata = null; + if (!isset($message->data[$consumed + $rdLength - 1])) { + return; + } - if (Message::TYPE_A === $type || Message::TYPE_AAAA === $type) { - $ip = substr($message->data, $consumed, $rdLength); - $consumed += $rdLength; + $rdata = null; + $expected = $consumed + $rdLength; - $rdata = inet_ntop($ip); + if (Message::TYPE_A === $type) { + if ($rdLength === 4) { + $rdata = inet_ntop(substr($message->data, $consumed, $rdLength)); + $consumed += $rdLength; + } + } elseif (Message::TYPE_AAAA === $type) { + if ($rdLength === 16) { + $rdata = inet_ntop(substr($message->data, $consumed, $rdLength)); + $consumed += $rdLength; + } } elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) { - list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed); - - $rdata = implode('.', $bodyLabels); + list($rdata, $consumed) = $this->readDomain($message->data, $consumed); } elseif (Message::TYPE_TXT === $type) { $rdata = array(); - $remaining = $rdLength; - while ($remaining) { + while ($consumed < $expected) { $len = ord($message->data[$consumed]); - $rdata[] = substr($message->data, $consumed + 1, $len); + $rdata[] = (string)substr($message->data, $consumed + 1, $len); $consumed += $len + 1; - $remaining -= $len + 1; } } elseif (Message::TYPE_MX === $type) { - list($priority) = array_values(unpack('n', substr($message->data, $consumed, 2))); - list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed + 2); - - $rdata = array( - 'priority' => $priority, - 'target' => implode('.', $bodyLabels) - ); + if ($rdLength > 2) { + list($priority) = array_values(unpack('n', substr($message->data, $consumed, 2))); + list($target, $consumed) = $this->readDomain($message->data, $consumed + 2); + + $rdata = array( + 'priority' => $priority, + 'target' => $target + ); + } } elseif (Message::TYPE_SRV === $type) { - list($priority, $weight, $port) = array_values(unpack('n*', substr($message->data, $consumed, 6))); - list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed + 6); - - $rdata = array( - 'priority' => $priority, - 'weight' => $weight, - 'port' => $port, - 'target' => implode('.', $bodyLabels) - ); + if ($rdLength > 6) { + list($priority, $weight, $port) = array_values(unpack('n*', substr($message->data, $consumed, 6))); + list($target, $consumed) = $this->readDomain($message->data, $consumed + 6); + + $rdata = array( + 'priority' => $priority, + 'weight' => $weight, + 'port' => $port, + 'target' => $target + ); + } } elseif (Message::TYPE_SOA === $type) { - list($primaryLabels, $consumed) = $this->readLabels($message->data, $consumed); - list($mailLabels, $consumed) = $this->readLabels($message->data, $consumed); - list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($message->data, $consumed, 20))); - $consumed += 20; - - $rdata = array( - 'mname' => implode('.', $primaryLabels), - 'rname' => implode('.', $mailLabels), - 'serial' => $serial, - 'refresh' => $refresh, - 'retry' => $retry, - 'expire' => $expire, - 'minimum' => $minimum - ); + list($mname, $consumed) = $this->readDomain($message->data, $consumed); + list($rname, $consumed) = $this->readDomain($message->data, $consumed); + + if ($mname !== null && $rname !== null && isset($message->data[$consumed + 20 - 1])) { + list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($message->data, $consumed, 20))); + $consumed += 20; + + $rdata = array( + 'mname' => $mname, + 'rname' => $rname, + 'serial' => $serial, + 'refresh' => $refresh, + 'retry' => $retry, + 'expire' => $expire, + 'minimum' => $minimum + ); + } } else { // unknown types simply parse rdata as an opaque binary string $rdata = substr($message->data, $consumed, $rdLength); $consumed += $rdLength; } + // ensure parsing record data consumes expact number of bytes indicated in record length + if ($consumed !== $expected || $rdata === null) { + return; + } + $message->consumed = $consumed; - $name = implode('.', $labels); $record = new Record($name, $type, $class, $ttl, $rdata); $message->answers[] = $record; @@ -236,6 +237,17 @@ public function parseAnswer(Message $message) return $message; } + private function readDomain($data, $consumed) + { + list ($labels, $consumed) = $this->readLabels($data, $consumed); + + if ($labels === null) { + return array(null, null); + } + + return array(implode('.', $labels), $consumed); + } + private function readLabels($data, $consumed) { $labels = array(); @@ -262,6 +274,7 @@ private function readLabels($data, $consumed) $consumed += 2; list($newLabels) = $this->readLabels($data, $offset); + if ($newLabels === null) { return array(null, null); } diff --git a/tests/Protocol/ParserTest.php b/tests/Protocol/ParserTest.php index f3eb56db..aed4e454 100644 --- a/tests/Protocol/ParserTest.php +++ b/tests/Protocol/ParserTest.php @@ -512,7 +512,7 @@ public function testParseSOAResponse() $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 06 00 01"; // answer: type SOA, class IN $data .= "00 01 51 80"; // answer: ttl 86400 - $data .= "00 07"; // answer: rdlength 7 + $data .= "00 27"; // answer: rdlength 39 $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname) $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname) $data .= "78 49 28 D5 00 00 2a 30 00 00 0e 10"; // answer: rdata 2018060501, 10800, 3600 @@ -690,6 +690,219 @@ public function testParseInvalidOffsetPointerToStartOfMessageInQuestionNameThrow $this->parser->parseMessage($data); } + /** + * @expectedException InvalidArgumentException + */ + public function testParseIncompleteAnswerFieldsThrows() + { + $data = ""; + $data .= "72 62 81 80 00 01 00 01 00 00 00 00"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "c0 0c"; // answer: offset pointer to igor.io + + $data = $this->convertTcpDumpToBinary($data); + + $this->parser->parseMessage($data); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testParseIncompleteAnswerRecordDataThrows() + { + $data = ""; + $data .= "72 62 81 80 00 01 00 01 00 00 00 00"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "c0 0c"; // answer: offset pointer to igor.io + $data .= "00 01 00 01"; // answer: type A, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 04"; // answer: rdlength 4 + + $data = $this->convertTcpDumpToBinary($data); + + $this->parser->parseMessage($data); + } + + public function testParseInvalidNSResponseWhereDomainNameIsMissing() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 02 00 01"; // answer: type NS, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 00"; // answer: rdlength 0 + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(0, $response->answers); + } + + public function testParseInvalidAResponseWhereIPIsMissing() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 01 00 01"; // answer: type A, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 00"; // answer: rdlength 0 + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(0, $response->answers); + } + + public function testParseInvalidAAAAResponseWhereIPIsMissing() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 1c 00 01"; // answer: type AAAA, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 00"; // answer: rdlength 0 + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(0, $response->answers); + } + + public function testParseInvalidTXTResponseWhereTxtChunkExceedsLimit() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 10 00 01"; // answer: type TXT, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 06"; // answer: rdlength 6 + $data .= "06 68 65 6c 6c 6f 6f"; // answer: rdata length 6: helloo + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(0, $response->answers); + } + + public function testParseInvalidMXResponseWhereDomainNameIsIncomplete() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 0f 00 01"; // answer: type MX, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 08"; // answer: rdlength 8 + $data .= "00 0a 05 68 65 6c 6c 6f"; // answer: rdata priority 10: hello (missing label end) + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(0, $response->answers); + } + + public function testParseInvalidMXResponseWhereDomainNameIsMissing() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 0f 00 01"; // answer: type MX, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 02"; // answer: rdlength 2 + $data .= "00 0a"; // answer: rdata priority 10 + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(0, $response->answers); + } + + public function testParseInvalidSRVResponseWhereDomainNameIsIncomplete() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 21 00 01"; // answer: type SRV, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 0b"; // answer: rdlength 11 + $data .= "00 0a 00 14 1F 90 04 74 65 73 74"; // answer: rdata priority 10, weight 20, port 8080 test (missing label end) + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(0, $response->answers); + } + + public function testParseInvalidSRVResponseWhereDomainNameIsMissing() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 21 00 01"; // answer: type SRV, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 06"; // answer: rdlength 6 + $data .= "00 0a 00 14 1F 90"; // answer: rdata priority 10, weight 20, port 8080 + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(0, $response->answers); + } + + public function testParseInvalidSOAResponseWhereFlagsAreMissing() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 06 00 01"; // answer: type SOA, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 13"; // answer: rdlength 19 + $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname) + $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname) + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(0, $response->answers); + } + private function convertTcpDumpToBinary($input) { // sudo ngrep -d en1 -x port 53