diff --git a/src/Neuron/Net/Outputs/HTML.php b/src/Neuron/Net/Outputs/HTML.php index bb3118d..ffe1e05 100644 --- a/src/Neuron/Net/Outputs/HTML.php +++ b/src/Neuron/Net/Outputs/HTML.php @@ -59,7 +59,9 @@ public function outputContent (Response $response) { if (!is_string ($response->getData ())) { - print_r ($response->getData ()); + // A data structure shown in an HTML response is a dump of + // values, not markup. + echo htmlspecialchars (print_r ($response->getData (), true), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } else diff --git a/src/Neuron/Net/Outputs/PrintR.php b/src/Neuron/Net/Outputs/PrintR.php index 155151b..14a6286 100644 --- a/src/Neuron/Net/Outputs/PrintR.php +++ b/src/Neuron/Net/Outputs/PrintR.php @@ -18,16 +18,25 @@ public function __construct () } + /** + * @param mixed $text + * @return string + */ + private static function escape ($text) + { + return htmlspecialchars ((string) $text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + } + public function outputContent (Response $response) { if (!is_string ($response->getData ())) { - echo '
' . print_r ($response->getData ()) . '
';
+			echo '
' . self::escape (print_r ($response->getData (), true)) . '
'; } else { - echo $response->getData (); + echo '
' . self::escape ($response->getData ()) . '
'; } } diff --git a/src/Neuron/Net/Outputs/Table.php b/src/Neuron/Net/Outputs/Table.php index 6fba5d9..2ca88b7 100644 --- a/src/Neuron/Net/Outputs/Table.php +++ b/src/Neuron/Net/Outputs/Table.php @@ -20,7 +20,9 @@ public function __construct () private function printTable ($data, $var_dump = true) { - header ('Content-type: text/html, charset=utf-8'); + if (!headers_sent ()) { + header ('Content-type: text/html; charset=utf-8'); + } echo ''; echo ''; @@ -51,7 +53,7 @@ private function printTableInner ($data, $var_dump = true) { echo ''; - echo '' . $k . ''; + echo '' . self::escape ($k) . ''; echo ''; if ($k === 'debug') @@ -71,17 +73,30 @@ private function printTableInner ($data, $var_dump = true) } else { + // The table is a view on data: every key and value is text and + // is escaped, whatever it contains. if ($var_dump) { + ob_start (); var_dump ($data); + echo self::escape (ob_get_clean ()); } else { - echo $data; + echo self::escape (is_scalar ($data) || $data === null ? (string) $data : print_r ($data, true)); } } } + /** + * @param mixed $text + * @return string + */ + private static function escape ($text) + { + return htmlspecialchars ((string) $text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + } + public function outputContent (Response $response) { if (!is_string ($response->getData ())) @@ -91,7 +106,7 @@ public function outputContent (Response $response) else { - echo $response->getData (); + echo self::escape ($response->getData ()); } } diff --git a/src/Neuron/Tools/TokenGenerator.php b/src/Neuron/Tools/TokenGenerator.php index e1bc0ee..01e911d 100644 --- a/src/Neuron/Tools/TokenGenerator.php +++ b/src/Neuron/Tools/TokenGenerator.php @@ -18,7 +18,7 @@ public static function getToken ($length) $out = ''; for ($i = 0; $i < $length; $i ++) { - $out .= substr ($range, mt_rand (0, strlen ($range) - 1), 1); + $out .= substr ($range, random_int (0, strlen ($range) - 1), 1); } return $out; } @@ -30,7 +30,7 @@ public static function getSimplifiedToken ($length) $out = ''; for ($i = 0; $i < $length; $i ++) { - $out .= substr ($range, mt_rand (0, strlen ($range) - 1), 1); + $out .= substr ($range, random_int (0, strlen ($range) - 1), 1); } return $out; } diff --git a/tests/OutputEscapingTest.php b/tests/OutputEscapingTest.php new file mode 100644 index 0000000..3245775 --- /dev/null +++ b/tests/OutputEscapingTest.php @@ -0,0 +1,116 @@ +setData ($data); + + ob_start (); + try { + $output->outputContent ($response); + } finally { + $out = ob_get_clean (); + } + + return $out; + } + + public function testTableEscapesKeysAndValues () + { + $out = $this->render (new Table (), array ('key' => 'value')); + + $this->assertStringNotContainsString ('key', $out); + $this->assertStringNotContainsString ('value', $out); + $this->assertStringContainsString ('<i>key</i>', $out); + $this->assertStringContainsString ('<b>value</b>', $out); + } + + public function testTableEscapesNestedAndDebugValues () + { + $out = $this->render (new Table (), array ( + 'row' => array ('name' => "it's x"), + 'debug' => array ('log' => 'line'), + )); + + $this->assertStringNotContainsString ('x', $out); + $this->assertStringNotContainsString ('line', $out); + $this->assertStringContainsString ('it's <b>x</b>', $out); + $this->assertStringContainsString ('<b>line</b>', $out); + } + + public function testTableKeepsItsOwnMarkup () + { + $out = $this->render (new Table (), array ('a' => 1)); + + $this->assertStringContainsString ('', $out); + $this->assertStringContainsString ('', $out); + } + + public function testTableEscapesStringData () + { + $out = $this->render (new Table (), 'text'); + + $this->assertSame ('<b>text</b>', $out); + } + + public function testPrintREscapesAndPrintsTheData () + { + $out = $this->render (new PrintR (), array ('name' => 'value')); + + $this->assertStringStartsWith ('
', $out);
+		$this->assertStringEndsWith ('
', $out); + $this->assertStringContainsString ('[name] => <b>value</b>', $out); + $this->assertStringNotContainsString ('value', $out); + } + + public function testHtmlOutputEscapesADataStructure () + { + $out = $this->render (new HTML (), array ('message' => 'value')); + + $this->assertStringNotContainsString ('value', $out); + $this->assertStringContainsString ('<b>value</b>', $out); + } + + public function testHtmlOutputLeavesABodyAlone () + { + $response = new Response (); + $response->setBody ('

page

'); + + ob_start (); + (new HTML ())->outputContent ($response); + $out = ob_get_clean (); + + $this->assertSame ('

page

', $out); + } + + public function testTokensKeepTheirAlphabetAndLength () + { + for ($i = 0; $i < 50; $i ++) { + $this->assertMatchesRegularExpression ('/\A[a-zA-Z0-9]{40}\z/', TokenGenerator::getToken (40)); + $this->assertMatchesRegularExpression ('/\A[A-Z0-9]{12}\z/', TokenGenerator::getSimplifiedToken (12)); + } + } + + public function testTokenGeneratorUsesTheSecureRandomSource () + { + $reflection = new \ReflectionClass (TokenGenerator::class); + $source = file_get_contents ($reflection->getFileName ()); + + $this->assertStringContainsString ('random_int', $source); + $this->assertStringNotContainsString ('mt_rand', $source); + } +}
a