cakephp2-php8/lib/Cake/Test/Case/Utility/DebuggerTest.php
mark_story edfda47cf4 Fix missing HTML encoding in Debugger
Fix missing HTML encoding when error messages contain HTML. This can
happen when user data is used as an offset in an array in an unchecked
way.

Thanks to Teppei Fukuda for reporting this issue via the responsible
security disclosure process.
2016-12-10 08:47:13 -05:00

651 lines
16 KiB
PHP

<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP Project
* @since CakePHP(tm) v 1.2.0.5432
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
App::uses('Debugger', 'Utility');
/**
* DebuggerTestCaseDebugger class
*
* @package Cake.Test.Case.Utility
*/
class DebuggerTestCaseDebugger extends Debugger {
}
/**
* DebuggerTest class
*
* !!! Be careful with changing code below as it may
* !!! change line numbers which are used in the tests
*
* @package Cake.Test.Case.Utility
*/
class DebuggerTest extends CakeTestCase {
protected $_restoreError = false;
/**
* setUp method
*
* @return void
*/
public function setUp() {
parent::setUp();
Configure::write('debug', 2);
Configure::write('log', false);
}
/**
* tearDown method
*
* @return void
*/
public function tearDown() {
parent::tearDown();
Configure::write('log', true);
if ($this->_restoreError) {
restore_error_handler();
}
}
/**
* testDocRef method
*
* @return void
*/
public function testDocRef() {
ini_set('docref_root', '');
$this->assertEquals(ini_get('docref_root'), '');
new Debugger();
$this->assertEquals(ini_get('docref_root'), 'http://php.net/');
}
/**
* test Excerpt writing
*
* @return void
*/
public function testExcerpt() {
$result = Debugger::excerpt(__FILE__, __LINE__, 2);
$this->assertTrue(is_array($result));
$this->assertEquals(5, count($result));
$this->assertRegExp('/function(.+)testExcerpt/', $result[1]);
$result = Debugger::excerpt(__FILE__, 2, 2);
$this->assertTrue(is_array($result));
$this->assertEquals(4, count($result));
$pattern = '/<code>.*?<span style\="color\: \#\d+">.*?&lt;\?php/';
$this->assertRegExp($pattern, $result[0]);
$result = Debugger::excerpt(__FILE__, 11, 2);
$this->assertEquals(5, count($result));
$pattern = '/<span style\="color\: \#\d{6}">\*<\/span>/';
$this->assertRegExp($pattern, $result[0]);
$return = Debugger::excerpt('[internal]', 2, 2);
$this->assertTrue(empty($return));
}
/**
* testOutput method
*
* @return void
*/
public function testOutput() {
set_error_handler('Debugger::showError');
$this->_restoreError = true;
$result = Debugger::output(false);
$this->assertEquals('', $result);
$out .= '';
$result = Debugger::output(true);
$this->assertEquals('Notice', $result[0]['error']);
$this->assertRegExp('/Undefined variable\:\s+out/', $result[0]['description']);
$this->assertRegExp('/DebuggerTest::testOutput/i', $result[0]['trace']);
ob_start();
Debugger::output('txt');
$other .= '';
$result = ob_get_clean();
$this->assertRegExp('/Undefined variable:\s+other/', $result);
$this->assertRegExp('/Context:/', $result);
$this->assertRegExp('/DebuggerTest::testOutput/i', $result);
ob_start();
Debugger::output('html');
$wrong .= '';
$result = ob_get_clean();
$this->assertRegExp('/<pre class="cake-error">.+<\/pre>/', $result);
$this->assertRegExp('/<b>Notice<\/b>/', $result);
$this->assertRegExp('/variable:\s+wrong/', $result);
ob_start();
Debugger::output('js');
$buzz .= '';
$result = explode('</a>', ob_get_clean());
$this->assertTags($result[0], array(
'pre' => array('class' => 'cake-error'),
'a' => array(
'href' => "javascript:void(0);",
'onclick' => "preg:/document\.getElementById\('cakeErr[a-z0-9]+\-trace'\)\.style\.display = " .
"\(document\.getElementById\('cakeErr[a-z0-9]+\-trace'\)\.style\.display == 'none'" .
" \? '' \: 'none'\);/"
),
'b' => array(), 'Notice', '/b', ' (8)',
));
$this->assertRegExp('/Undefined variable:\s+buzz/', $result[1]);
$this->assertRegExp('/<a[^>]+>Code/', $result[1]);
$this->assertRegExp('/<a[^>]+>Context/', $result[2]);
$this->assertContains('$wrong = &#039;&#039;', $result[3], 'Context should be HTML escaped.');
}
/**
* test encodes error messages
*
* @return void
*/
public function testOutputEncodeDescription() {
set_error_handler('Debugger::showError');
$this->_restoreError = true;
ob_start();
$a = 'things';
$b = $a['<script>alert(1)</script>'];
$result = ob_get_clean();
$this->assertNotContains('<script>alert(1)', $result);
$this->assertContains('&lt;script&gt;alert(1)', $result);
}
/**
* Tests that changes in output formats using Debugger::output() change the templates used.
*
* @return void
*/
public function testChangeOutputFormats() {
set_error_handler('Debugger::showError');
$this->_restoreError = true;
Debugger::output('js', array(
'traceLine' => '{:reference} - <a href="txmt://open?url=file://{:file}' .
'&line={:line}">{:path}</a>, line {:line}'
));
$result = Debugger::trace();
$this->assertRegExp('/' . preg_quote('txmt://open?url=file://', '/') . '(\/|[A-Z]:\\\\)' . '/', $result);
Debugger::output('xml', array(
'error' => '<error><code>{:code}</code><file>{:file}</file><line>{:line}</line>' .
'{:description}</error>',
'context' => "<context>{:context}</context>",
'trace' => "<stack>{:trace}</stack>",
));
Debugger::output('xml');
ob_start();
$foo .= '';
$result = ob_get_clean();
$data = array(
'error' => array(),
'code' => array(), '8', '/code',
'file' => array(), 'preg:/[^<]+/', '/file',
'line' => array(), '' . ((int)__LINE__ - 7), '/line',
'preg:/Undefined variable:\s+foo/',
'/error'
);
$this->assertTags($result, $data, true);
}
/**
* Test that outputAs works.
*
* @return void
*/
public function testOutputAs() {
Debugger::outputAs('html');
$this->assertEquals('html', Debugger::outputAs());
}
/**
* Test that choosing a non-existent format causes an exception
*
* @expectedException CakeException
* @return void
*/
public function testOutputAsException() {
Debugger::outputAs('Invalid junk');
}
/**
* Tests that changes in output formats using Debugger::output() change the templates used.
*
* @return void
*/
public function testAddFormat() {
set_error_handler('Debugger::showError');
$this->_restoreError = true;
Debugger::addFormat('js', array(
'traceLine' => '{:reference} - <a href="txmt://open?url=file://{:file}' .
'&line={:line}">{:path}</a>, line {:line}'
));
Debugger::outputAs('js');
$result = Debugger::trace();
$this->assertRegExp('/' . preg_quote('txmt://open?url=file://', '/') . '(\/|[A-Z]:\\\\)' . '/', $result);
Debugger::addFormat('xml', array(
'error' => '<error><code>{:code}</code><file>{:file}</file><line>{:line}</line>' .
'{:description}</error>',
));
Debugger::outputAs('xml');
ob_start();
$foo .= '';
$result = ob_get_clean();
$data = array(
'<error',
'<code', '8', '/code',
'<file', 'preg:/[^<]+/', '/file',
'<line', '' . ((int)__LINE__ - 7), '/line',
'preg:/Undefined variable:\s+foo/',
'/error'
);
$this->assertTags($result, $data, true);
}
/**
* Test adding a format that is handled by a callback.
*
* @return void
*/
public function testAddFormatCallback() {
set_error_handler('Debugger::showError');
$this->_restoreError = true;
Debugger::addFormat('callback', array('callback' => array($this, 'customFormat')));
Debugger::outputAs('callback');
ob_start();
$foo .= '';
$result = ob_get_clean();
$this->assertContains('Notice: I eated an error', $result);
$this->assertContains('DebuggerTest.php', $result);
}
/**
* Test method for testing addFormat with callbacks.
*
* @return void
*/
public function customFormat($error, $strings) {
return $error['error'] . ': I eated an error ' . $error['file'];
}
/**
* testTrimPath method
*
* @return void
*/
public function testTrimPath() {
$this->assertEquals('APP' . DS, Debugger::trimPath(APP));
$this->assertEquals('CORE', Debugger::trimPath(CAKE_CORE_INCLUDE_PATH));
$this->assertEquals('ROOT', Debugger::trimPath(ROOT));
$this->assertEquals('CORE' . DS . 'Cake' . DS, Debugger::trimPath(CAKE));
$this->assertEquals('Some/Other/Path', Debugger::trimPath('Some/Other/Path'));
}
/**
* testExportVar method
*
* @return void
*/
public function testExportVar() {
App::uses('Controller', 'Controller');
$Controller = new Controller();
$Controller->helpers = array('Html', 'Form');
$View = new View($Controller);
$View->int = 2;
$View->float = 1.333;
$result = Debugger::exportVar($View);
$expected = <<<TEXT
object(View) {
Helpers => object(HelperCollection) {}
Blocks => object(ViewBlock) {}
plugin => null
name => ''
passedArgs => array()
helpers => array(
(int) 0 => 'Html',
(int) 1 => 'Form'
)
viewPath => ''
viewVars => array()
view => null
layout => 'default'
layoutPath => null
autoLayout => true
ext => '.ctp'
subDir => null
theme => null
cacheAction => false
validationErrors => array()
hasRendered => false
uuids => array()
request => object(CakeRequest) {}
response => object(CakeResponse) {}
elementCache => 'default'
elementCacheSettings => array()
Html => object(HtmlHelper) {}
Form => object(FormHelper) {}
int => (int) 2
float => (float) 1.333
TEXT;
if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
$expected .= <<<TEXT
[protected] _passedVars => array(
(int) 0 => 'viewVars',
(int) 1 => 'autoLayout',
(int) 2 => 'ext',
(int) 3 => 'helpers',
(int) 4 => 'view',
(int) 5 => 'layout',
(int) 6 => 'name',
(int) 7 => 'theme',
(int) 8 => 'layoutPath',
(int) 9 => 'viewPath',
(int) 10 => 'request',
(int) 11 => 'plugin',
(int) 12 => 'passedArgs',
(int) 13 => 'cacheAction'
)
[protected] _scripts => array()
[protected] _paths => array()
[protected] _pathsForPlugin => array()
[protected] _parents => array()
[protected] _current => null
[protected] _currentType => ''
[protected] _stack => array()
[protected] _eventManager => object(CakeEventManager) {}
[protected] _eventManagerConfigured => false
TEXT;
}
$expected .= <<<TEXT
}
TEXT;
$this->assertTextEquals($expected, $result);
$data = array(
1 => 'Index one',
5 => 'Index five'
);
$result = Debugger::exportVar($data);
$expected = <<<TEXT
array(
(int) 1 => 'Index one',
(int) 5 => 'Index five'
)
TEXT;
$this->assertTextEquals($expected, $result);
$data = array(
'key' => array(
'value'
)
);
$result = Debugger::exportVar($data, 1);
$expected = <<<TEXT
array(
'key' => array(
[maximum depth reached]
)
)
TEXT;
$this->assertTextEquals($expected, $result);
$data = false;
$result = Debugger::exportVar($data);
$expected = <<<TEXT
false
TEXT;
$this->assertTextEquals($expected, $result);
$file = fopen('php://output', 'w');
fclose($file);
$result = Debugger::exportVar($file);
$this->assertTextEquals('unknown', $result);
}
/**
* Test exporting various kinds of false.
*
* @return void
*/
public function testExportVarZero() {
$data = array(
'nothing' => '',
'null' => null,
'false' => false,
'szero' => '0',
'zero' => 0
);
$result = Debugger::exportVar($data);
$expected = <<<TEXT
array(
'nothing' => '',
'null' => null,
'false' => false,
'szero' => '0',
'zero' => (int) 0
)
TEXT;
$this->assertTextEquals($expected, $result);
}
/**
* testLog method
*
* @return void
*/
public function testLog() {
if (file_exists(LOGS . 'debug.log')) {
unlink(LOGS . 'debug.log');
}
CakeLog::config('file', array('engine' => 'File', 'path' => TMP . 'logs' . DS));
Debugger::log('cool');
$result = file_get_contents(LOGS . 'debug.log');
$this->assertContains('DebuggerTest::testLog', $result);
$this->assertContains("'cool'", $result);
unlink(LOGS . 'debug.log');
Debugger::log(array('whatever', 'here'));
$result = file_get_contents(LOGS . 'debug.log');
$this->assertContains('DebuggerTest::testLog', $result);
$this->assertContains('[main]', $result);
$this->assertContains('array', $result);
$this->assertContains("'whatever',", $result);
$this->assertContains("'here'", $result);
}
/**
* test log() depth
*
* @return void
*/
public function testLogDepth() {
if (file_exists(LOGS . 'debug.log')) {
unlink(LOGS . 'debug.log');
}
CakeLog::config('file', array('engine' => 'File', 'path' => TMP . 'logs' . DS));
$val = array(
'test' => array('key' => 'val')
);
Debugger::log($val, LOG_DEBUG, 0);
$result = file_get_contents(LOGS . 'debug.log');
$this->assertContains('DebuggerTest::testLog', $result);
$this->assertNotContains("/'val'/", $result);
unlink(LOGS . 'debug.log');
}
/**
* testDump method
*
* @return void
*/
public function testDump() {
$var = array('People' => array(
array(
'name' => 'joeseph',
'coat' => 'technicolor',
'hair_color' => 'brown'
),
array(
'name' => 'Shaft',
'coat' => 'black',
'hair' => 'black'
)
));
ob_start();
Debugger::dump($var);
$result = ob_get_clean();
$open = PHP_SAPI === 'cli' ? "\n" : '<pre>';
$close = PHP_SAPI === 'cli' ? "\n" : '</pre>';
$expected = <<<TEXT
{$open}array(
'People' => array(
(int) 0 => array(
'name' => 'joeseph',
'coat' => 'technicolor',
'hair_color' => 'brown'
),
(int) 1 => array(
'name' => 'Shaft',
'coat' => 'black',
'hair' => 'black'
)
)
){$close}
TEXT;
$this->assertTextEquals($expected, $result);
ob_start();
Debugger::dump($var, 1);
$result = ob_get_clean();
$open = PHP_SAPI === 'cli' ? "\n" : '<pre>';
$close = PHP_SAPI === 'cli' ? "\n" : '</pre>';
$expected = <<<TEXT
{$open}array(
'People' => array(
[maximum depth reached]
)
){$close}
TEXT;
$this->assertTextEquals($expected, $result);
}
/**
* test getInstance.
*
* @return void
*/
public function testGetInstance() {
$result = Debugger::getInstance();
$this->assertInstanceOf('Debugger', $result);
$result = Debugger::getInstance('DebuggerTestCaseDebugger');
$this->assertInstanceOf('DebuggerTestCaseDebugger', $result);
$result = Debugger::getInstance();
$this->assertInstanceOf('DebuggerTestCaseDebugger', $result);
$result = Debugger::getInstance('Debugger');
$this->assertInstanceOf('Debugger', $result);
}
/**
* testNoDbCredentials
*
* If a connection error occurs, the config variable is passed through exportVar
* *** our database login credentials such that they are never visible
*
* @return void
*/
public function testNoDbCredentials() {
$config = array(
'datasource' => 'mysql',
'persistent' => false,
'host' => 'void.cakephp.org',
'login' => 'cakephp-user',
'password' => 'cakephp-password',
'database' => 'cakephp-database',
'prefix' => ''
);
$output = Debugger::exportVar($config);
$expectedArray = array(
'datasource' => 'mysql',
'persistent' => false,
'host' => '*****',
'login' => '*****',
'password' => '*****',
'database' => '*****',
'prefix' => ''
);
$expected = Debugger::exportVar($expectedArray);
$this->assertEquals($expected, $output);
}
/**
* Test that exportVar() doesn't loop through recursive structures.
*
* @return void
*/
public function testExportVarRecursion() {
$output = Debugger::exportVar($GLOBALS);
$this->assertContains("'GLOBALS' => [recursion]", $output);
}
/**
* test trace exclude
*
* @return void
*/
public function testTraceExclude() {
$result = Debugger::trace();
$this->assertRegExp('/^DebuggerTest::testTraceExclude/', $result);
$result = Debugger::trace(array(
'exclude' => array('DebuggerTest::testTraceExclude')
));
$this->assertNotRegExp('/^DebuggerTest::testTraceExclude/', $result);
}
}