mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-11-02 10:44:30 -05:00
Cake logs every uncaught exception, 4xx included, with "Request URL:"
and the full query string. API clients may authenticate with ?token=,
?auth= or ?user=&pass=, so a 401, 404 or 500 wrote a working access
token, auth hash or password to cake_error.log and error.log in
ZM_LOGDIR, both world-readable.
ZmErrorHandler extends Cake's ErrorHandler and replaces the values of
token, auth, pass and password with [redacted] in the logged message.
core.php uses it for both errors and exceptions.
Credentials in the query string still reach web server and proxy
access logs; that is outside the API code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b80568847d)
76 lines
2.8 KiB
PHP
76 lines
2.8 KiB
PHP
<?php
|
|
// Tests what the API's error handler writes to cake_error.log:
|
|
// ZmErrorHandler::redact() - credential values in a URL or log text
|
|
// ZmErrorHandler::_getMessage() - the logged "Request URL:" must not carry
|
|
// token=, auth=, pass= or password= values
|
|
//
|
|
// Cake's ErrorHandler is stubbed, so this runs from a source checkout:
|
|
// php tests/php/test_api_error_log_redaction.php
|
|
|
|
class App {
|
|
public static function uses($class, $package) {}
|
|
}
|
|
|
|
// What Cake's ErrorHandler::_getMessage() builds: class, message, request URL.
|
|
class ErrorHandler {
|
|
public static $url = '';
|
|
protected static function _getMessage($exception) {
|
|
return '['.get_class($exception).'] '.$exception->getMessage()
|
|
."\nRequest URL: ".static::$url."\nStack Trace:\n#0 {main}";
|
|
}
|
|
}
|
|
|
|
require_once __DIR__.'/../../web/api/app/Lib/Error/ZmErrorHandler.php';
|
|
|
|
class TestErrorHandler extends ZmErrorHandler {
|
|
public static function message($exception) { return static::_getMessage($exception); }
|
|
}
|
|
|
|
$failures = 0;
|
|
$passes = 0;
|
|
|
|
function check($name, $got, $expected) {
|
|
global $failures, $passes;
|
|
if ($got === $expected) {
|
|
$passes++;
|
|
echo "ok - $name\n";
|
|
} else {
|
|
$failures++;
|
|
echo "not ok - $name (got ".var_export($got, true)." expected ".var_export($expected, true).")\n";
|
|
}
|
|
}
|
|
|
|
// ---- ZmErrorHandler::redact() ----
|
|
|
|
check('token value is redacted',
|
|
ZmErrorHandler::redact('/zm/api/monitors.json?token=eyJ0.eyJp.sig'),
|
|
'/zm/api/monitors.json?token=[redacted]');
|
|
check('user is kept, pass is redacted',
|
|
ZmErrorHandler::redact('/zm/api/host/login.json?user=admin&pass=s3cret&x=1'),
|
|
'/zm/api/host/login.json?user=admin&pass=[redacted]&x=1');
|
|
check('auth and password are redacted',
|
|
ZmErrorHandler::redact('/a?auth=abc123&password=pw'),
|
|
'/a?auth=[redacted]&password=[redacted]');
|
|
check('HTML-escaped separator is matched',
|
|
ZmErrorHandler::redact('/a?p=%2Fa&token=eyJ0.x.y'),
|
|
'/a?p=%2Fa&token=[redacted]');
|
|
check('names are matched whole, not as a suffix',
|
|
ZmErrorHandler::redact('/a?mytoken=1&passive=2&bypass=3'),
|
|
'/a?mytoken=1&passive=2&bypass=3');
|
|
check('text without credentials is unchanged',
|
|
ZmErrorHandler::redact('/zm/api/monitors/view/1.json?p=%2Fmonitors'),
|
|
'/zm/api/monitors/view/1.json?p=%2Fmonitors');
|
|
|
|
// ---- the logged message ----
|
|
|
|
ErrorHandler::$url = '/zm/api/monitors/view/9999.json?p=%2Fx&user=viewer&pass=s3cret&token=eyJ0.eyJp.sig';
|
|
$logged = TestErrorHandler::message(new Exception('Invalid monitor'));
|
|
check('logged message has no password', strpos($logged, 's3cret'), false);
|
|
check('logged message has no token', strpos($logged, 'eyJ0'), false);
|
|
check('logged message keeps the path and user',
|
|
strpos($logged, 'Request URL: /zm/api/monitors/view/9999.json?p=%2Fx&user=viewer&pass=[redacted]') !== false, true);
|
|
|
|
echo "\n$passes passed, $failures failed\n";
|
|
exit($failures ? 1 : 0);
|
|
?>
|