new: [security] Content-Security-Policy support

pull/7097/head
Jakub Onderka 2021-02-25 22:04:50 +01:00
parent 38f785ea63
commit 8a3144f112
3 changed files with 75 additions and 3 deletions

View File

@ -123,6 +123,9 @@ class AppController extends Controller
if (Configure::read('Security.disable_browser_cache')) {
$this->response->disableCache();
}
if (!$this->_isRest()) {
$this->__contentSecurityPolicy();
}
$this->response->header('X-XSS-Protection', '1; mode=block');
if (!empty($this->params['named']['sql'])) {
@ -314,11 +317,11 @@ class AppController extends Controller
$this->__accessMonitor($user);
} else {
$pre_auth_actions = array('login', 'register', 'getGpgPublicKey');
$preAuthActions = array('login', 'register', 'getGpgPublicKey');
if (!empty(Configure::read('Security.email_otp_enabled'))) {
$pre_auth_actions[] = 'email_otp';
$preAuthActions[] = 'email_otp';
}
if (!$this->_isControllerAction(['users' => $pre_auth_actions])) {
if (!$this->_isControllerAction(['users' => $preAuthActions, 'servers' => ['cspReport']])) {
if (!$this->request->is('ajax')) {
$this->Session->write('pre_login_requested_url', $this->here);
}
@ -685,6 +688,50 @@ class AppController extends Controller
}
}
/**
* Generate Content-Security-Policy HTTP header
*/
private function __contentSecurityPolicy()
{
$default = [
'default-src' => "'self' data: 'unsafe-inline' 'unsafe-eval'",
'style-src' => "'self' 'unsafe-inline'",
'object-src' => "'none'",
'frame-ancestors' => "'none'",
'worker-src' => "'none'",
'child-src' => "'none'",
'frame-src' => "'none'",
'base-uri' => "'self'",
'img-src' => "'self' data:",
'font-src' => "'self'",
'form-action' => "'self'",
'connect-src' => "'self'",
'manifest-src' => "'none'",
'report-uri' => '/servers/cspReport',
];
if (env('HTTPS')) {
$default['upgrade-insecure-requests'] = null;
}
$custom = Configure::read('Security.csp');
if ($custom === false) {
return;
}
if (is_array($custom)) {
$default = $default + $custom;
}
$header = [];
foreach ($default as $key => $value) {
if ($value !== false) {
if ($value === null) {
$header[] = $key;
} else {
$header[] = "$key $value";
}
}
}
$this->response->header('Content-Security-Policy', implode('; ', $header));
}
private function __rateLimitCheck()
{
$info = array();

View File

@ -533,6 +533,7 @@ class ACLComponent extends Component
'uploadFile' => array(),
'viewDeprecatedFunctionUse' => array(),
'killAllWorkers' => ['perm_site_admin'],
'cspReport' => ['*'],
),
'shadowAttributes' => array(
'accept' => array('perm_add'),

View File

@ -35,8 +35,11 @@ class ServersController extends AppController
public function beforeFilter()
{
$this->Auth->allow(['cspReport']); // cspReport must work without authentication
parent::beforeFilter();
$this->Security->unlockedActions[] = 'getApiInfo';
$this->Security->unlockedActions[] = 'cspReport';
// permit reuse of CSRF tokens on some pages.
switch ($this->request->params['action']) {
case 'push':
@ -2419,6 +2422,27 @@ misp.direct_call(relative_path, body)
}
}
public function cspReport()
{
if (!$this->request->is('post')) {
throw new MethodNotAllowedException('This action expects a POST request.');
}
$report = $this->Server->jsonDecode($this->request->input());
if (!isset($report['csp-report'])) {
throw new RuntimeException("Invalid report");
}
$message = 'CSP reported violation';
$ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR';
if (isset($_SERVER[$ipHeader])) {
$message .= ' from IP ' . $_SERVER[$ipHeader];
}
$this->log("$message: " . json_encode($report['csp-report'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
return new CakeResponse(['statusCodes' => 204]);
}
public function viewDeprecatedFunctionUse()
{
$data = $this->Deprecation->getDeprecatedAccessList($this->Server);