mirror of
https://github.com/getgrav/grav.git
synced 2025-02-20 19:56:53 +01:00
Merge branch 'develop' into 1.6
# Conflicts: # .gitignore # bin/grav
This commit is contained in:
commit
d893dd55ff
|
|
@ -26,7 +26,8 @@
|
|||
1. [](#new)
|
||||
* Added `Deprecated` tab to DebugBar to catch future incompatibilities with later Grav versions
|
||||
* Added deprecation notices for features which will be removed in Grav 2.0
|
||||
* Added `Utils::detectXssFromArray()` and `Utils::detectXss()` methods
|
||||
* Added new `bin/grav security` command to scan for security issues (XSS currently)
|
||||
* Added new `Security` class for Grav security functionality
|
||||
* Added `onHttpPostFilter` event to allow plugins to globally clean up XSS in the forms and tasks
|
||||
1. [](#bugfix)
|
||||
* Allow `$page->slug()` to be called before `$page->init()` without breaking the page
|
||||
|
|
|
|||
1
bin/grav
1
bin/grav
|
|
@ -43,5 +43,6 @@ $app->addCommands(array(
|
|||
new \Grav\Console\Cli\BackupCommand(),
|
||||
new \Grav\Console\Cli\NewProjectCommand(),
|
||||
new \Grav\Console\Cli\SchedulerCommand(),
|
||||
new \Grav\Console\Cli\SecurityCommand(),
|
||||
));
|
||||
$app->run();
|
||||
|
|
|
|||
50
system/blueprints/config/security.yaml
Normal file
50
system/blueprints/config/security.yaml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
title: PLUGIN_ADMIN.SECURITY
|
||||
|
||||
form:
|
||||
validation: loose
|
||||
fields:
|
||||
|
||||
security_section:
|
||||
type: section
|
||||
title: PLUGIN_ADMIN.XSS_SECURITY
|
||||
underline: true
|
||||
|
||||
xss_whitelist:
|
||||
type: selectize
|
||||
size: large
|
||||
label: PLUGIN_ADMIN.XSS_WHITELIST_PERMISSIONS
|
||||
help: PLUGIN_ADMIN.XSS_WHITELIST_PERMISSIONS_HELP
|
||||
placeholder: 'admin.super'
|
||||
classes: fancy
|
||||
validate:
|
||||
type: commalist
|
||||
|
||||
|
||||
xss_rules:
|
||||
type: list
|
||||
style: vertical
|
||||
label: PLUGIN_ADMIN.XSS_RULES
|
||||
help: PLUGIN_ADMIN.XSS_RULES_HELP
|
||||
classes: compact
|
||||
fields:
|
||||
.label:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.XSS_RULE_LABEL
|
||||
validate:
|
||||
required: true
|
||||
.regex:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.XSS_RULE_REGEX
|
||||
validate:
|
||||
required: true
|
||||
.enabled:
|
||||
type: toggle
|
||||
label: PLUGIN_ADMIN.ENABLED
|
||||
highlight: 1
|
||||
options:
|
||||
1: PLUGIN_ADMIN.YES
|
||||
0: PLUGIN_ADMIN.NO
|
||||
default: true
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
22
system/config/security.yaml
Normal file
22
system/config/security.yaml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
xss_whitelist: [admin.super] # Whitelist of user access that should 'skip' XSS checking
|
||||
xss_rules: # Array of XSS tests to run through
|
||||
-
|
||||
label: On-Events
|
||||
enabled: true
|
||||
regex: '#(<[^>]+[\x00-\x20\"''\/])(on|xmlns)[^>]*>?#iUu'
|
||||
-
|
||||
label: JavaScript
|
||||
enabled: true
|
||||
regex: '!((java|live|vb)script|mocha|feed|data):(\w)*!iUu'
|
||||
-
|
||||
label: Moz-Binding
|
||||
enabled: true
|
||||
regex: '#-moz-binding[\x00-\x20]*:#u'
|
||||
-
|
||||
label: 'Style Attributes'
|
||||
enabled: false
|
||||
regex: '#(<[^>]+[\x00-\x20\"''\/])style=[^>]*>?#iUu'
|
||||
-
|
||||
label: 'Dangerous Tags'
|
||||
enabled: true
|
||||
regex: '#</*(applet|meta|xml|blink|link|style|script|embed|object|iframe|frame|frameset|ilayer|layer|bgsound|title|base)[^>]*>?#ui'
|
||||
138
system/src/Grav/Common/Security.php
Normal file
138
system/src/Grav/Common/Security.php
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
/**
|
||||
* @package Grav.Common
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common;
|
||||
|
||||
class Security
|
||||
{
|
||||
|
||||
public static function detectXssFromPages($pages, callable $status = null)
|
||||
{
|
||||
$routes = $pages->routes();
|
||||
|
||||
// Remove duplicate for homepage
|
||||
unset($routes['/']);
|
||||
|
||||
$list = [];
|
||||
|
||||
// // This needs Symfony 4.1 to work
|
||||
// $status && $status([
|
||||
// 'type' => 'count',
|
||||
// 'steps' => count($routes),
|
||||
// ]);
|
||||
|
||||
foreach ($routes as $route => $path) {
|
||||
|
||||
$status && $status([
|
||||
'type' => 'progress',
|
||||
]);
|
||||
|
||||
try {
|
||||
$page = $pages->get($path);
|
||||
|
||||
// call the content to load/cache it
|
||||
$header = (array) $page->header();
|
||||
$content = $page->content();
|
||||
|
||||
$data = ['header' => $header, 'content' => $content];
|
||||
$results = Security::detectXssFromArray($data);
|
||||
|
||||
if (!empty($results)) {
|
||||
$list[$route] = $results;
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array Array such as $_POST or $_GET
|
||||
* @param string $prefix Prefix for returned values.
|
||||
* @return array Returns flatten list of potentially dangerous input values, such as 'data.content'.
|
||||
*/
|
||||
public static function detectXssFromArray(array $array, $prefix = '')
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($array as $key => $value) {
|
||||
if (\is_array($value)) {
|
||||
$list[] = static::detectXssFromArray($value, $prefix . $key . '.');
|
||||
}
|
||||
if ($result = static::detectXss($value)) {
|
||||
$list[] = [$prefix . $key => $result];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($list)) {
|
||||
return array_merge(...$list);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if string potentially has a XSS attack. This simple function does not catch all XSS and it is likely to
|
||||
* return false positives because of it tags all potentially dangerous HTML tags and attributes without looking into
|
||||
* their content.
|
||||
*
|
||||
* @param string $string The string to run XSS detection logic on
|
||||
* @return boolean|string Type of XSS vector if the given `$string` may contain XSS, false otherwise.
|
||||
*
|
||||
* Copies the code from: https://github.com/symphonycms/xssfilter/blob/master/extension.driver.php#L138
|
||||
*/
|
||||
public static function detectXss($string)
|
||||
{
|
||||
// Skip any null or non string values
|
||||
if (null === $string || !\is_string($string) || empty($string)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep a copy of the original string before cleaning up
|
||||
$orig = $string;
|
||||
|
||||
// URL decode
|
||||
$string = urldecode($string);
|
||||
|
||||
// Convert Hexadecimals
|
||||
$string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', function($m) {
|
||||
return \chr(hexdec($m[2]));
|
||||
}, $string);
|
||||
|
||||
// Clean up entities
|
||||
$string = preg_replace('!(�+[0-9]+)!u','$1;', $string);
|
||||
|
||||
// Decode entities
|
||||
$string = html_entity_decode($string, ENT_NOQUOTES, 'UTF-8');
|
||||
|
||||
// Strip whitespace characters
|
||||
$string = preg_replace('!\s!u','', $string);
|
||||
|
||||
// Get XSS rules from security configuration
|
||||
$xss_rules = Grav::instance()['config']->get('security.xss_rules');
|
||||
|
||||
// Iterate over rules and return label if fail
|
||||
foreach ((array) $xss_rules as $rule) {
|
||||
if ($rule['enabled'] === true) {
|
||||
$label = $rule['label'];
|
||||
$regex = $rule['regex'];
|
||||
|
||||
if ($label && $regex) {
|
||||
if (preg_match($regex, $string) || preg_match($regex, $orig)) {
|
||||
return $label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -253,90 +253,6 @@ abstract class Utils
|
|||
return $date_formats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array Array such as $_POST or $_GET
|
||||
* @param string $prefix Prefix for returned values.
|
||||
* @return array Returns flatten list of potentially dangerous input values, such as 'data.content'.
|
||||
*/
|
||||
public static function detectXssFromArray(array $array, $prefix = '')
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($array as $key => $value) {
|
||||
if (\is_array($value)) {
|
||||
$list[] = static::detectXssFromArray($value, $prefix . $key . '.');
|
||||
}
|
||||
if (static::detectXss($value)) {
|
||||
$list[] = [$prefix . $key];
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge(...$list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if string potentially has a XSS attack. This simple function does not catch all XSS and it is likely to
|
||||
* return false positives because of it tags all potentially dangerous HTML tags and attributes without looking into
|
||||
* their content.
|
||||
*
|
||||
* @param string $string The string to run XSS detection logic on
|
||||
* @return boolean True if the given `$string` may contain XSS, false otherwise.
|
||||
*
|
||||
* Copies the code from: https://github.com/symphonycms/xssfilter/blob/master/extension.driver.php#L138
|
||||
*/
|
||||
public static function detectXss($string)
|
||||
{
|
||||
// Skip any null or non string values
|
||||
if (null === $string || !\is_string($string) || empty($string)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep a copy of the original string before cleaning up
|
||||
$orig = $string;
|
||||
|
||||
// URL decode
|
||||
$string = urldecode($string);
|
||||
|
||||
// Convert Hexadecimals
|
||||
$string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', function($m) {
|
||||
return \chr(hexdec($m[2]));
|
||||
}, $string);
|
||||
|
||||
// Clean up entities
|
||||
$string = preg_replace('!(�+[0-9]+)!u','$1;', $string);
|
||||
|
||||
// Decode entities
|
||||
$string = html_entity_decode($string, ENT_NOQUOTES, 'UTF-8');
|
||||
|
||||
// Strip whitespace characters
|
||||
$string = preg_replace('!\s!u','', $string);
|
||||
|
||||
// Set the patterns we'll test against
|
||||
$patterns = [
|
||||
// Match any attribute starting with "on" or xmlns
|
||||
'#(<[^>]+[\x00-\x20\"\'\/])(on|xmlns)[^>]*>?#iUu',
|
||||
|
||||
// Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols
|
||||
'!((java|live|vb)script|mocha|feed|data):(\w)*!iUu',
|
||||
|
||||
// Match -moz-bindings
|
||||
'#-moz-binding[\x00-\x20]*:#u',
|
||||
|
||||
// Match style attributes
|
||||
'#(<[^>]+[\x00-\x20\"\'\/])style=[^>]*>?#iUu',
|
||||
|
||||
// Match potentially dangerous tags
|
||||
'#</*(applet|meta|xml|blink|link|style|script|embed|object|iframe|frame|frameset|ilayer|layer|bgsound|title|base)[^>]*>?#ui'
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
// Test both the original string and clean string
|
||||
if (preg_match($pattern, $string) || preg_match($pattern, $orig)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Truncate text by number of characters but can cut off words.
|
||||
*
|
||||
|
|
|
|||
113
system/src/Grav/Console/Cli/SecurityCommand.php
Normal file
113
system/src/Grav/Console/Cli/SecurityCommand.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
/**
|
||||
* @package Grav.Console
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Console\Cli;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Security;
|
||||
use Grav\Console\ConsoleCommand;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class SecurityCommand extends ConsoleCommand
|
||||
{
|
||||
/** @var ProgressBar $progress */
|
||||
protected $progress;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName("security")
|
||||
->setDescription("Capable of running various Security checks")
|
||||
->setHelp('The <info>security</info> runs various security checks on your Grav site');
|
||||
|
||||
$this->source = getcwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null|void
|
||||
*/
|
||||
protected function serve()
|
||||
{
|
||||
|
||||
|
||||
/** @var Grav $grav */
|
||||
$grav = Grav::instance();
|
||||
|
||||
$grav['uri']->init();
|
||||
$grav['config']->init();
|
||||
$grav['debugger']->enabled(false);
|
||||
$grav['streams'];
|
||||
$grav['plugins']->init();
|
||||
$grav['themes']->init();
|
||||
|
||||
|
||||
$grav['twig']->init();
|
||||
$grav['pages']->init();
|
||||
|
||||
$this->progress = new ProgressBar($this->output, (count($grav['pages']->routes()) - 1));
|
||||
$this->progress->setFormat('Scanning <cyan>%current%</cyan> pages [<green>%bar%</green>] <white>%percent:3s%%</white> %elapsed:6s%');
|
||||
$this->progress->setBarWidth(100);
|
||||
|
||||
$io = new SymfonyStyle($this->input, $this->output);
|
||||
$io->title('Grav Security Check');
|
||||
|
||||
$output = Security::detectXssFromPages($grav['pages'], [$this, 'outputProgress']);
|
||||
|
||||
$io->newline(2);
|
||||
|
||||
if (!empty($output)) {
|
||||
|
||||
$counter = 1;
|
||||
foreach ($output as $route => $results) {
|
||||
|
||||
$results_parts = array_map(function($value, $key) {
|
||||
return $key.': \''.$value . '\'';
|
||||
}, array_values($results), array_keys($results));
|
||||
|
||||
$io->writeln($counter++ .' - <cyan>' . $route . '</cyan> → <red>' . implode(', ', $results_parts) . '</red>');
|
||||
}
|
||||
|
||||
$io->error('Security Scan complete: ' . count($output) . ' potential XSS issues found...');
|
||||
|
||||
} else {
|
||||
$io->success('Security Scan complete: No issues found...');
|
||||
}
|
||||
|
||||
$io->newline(1);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $args
|
||||
*/
|
||||
public function outputProgress($args)
|
||||
{
|
||||
switch ($args['type']) {
|
||||
case 'count':
|
||||
$steps = $args['steps'];
|
||||
$freq = intval($steps > 100 ? round($steps / 100) : $steps);
|
||||
$this->progress->setMaxSteps($steps);
|
||||
$this->progress->setRedrawFrequency($freq);
|
||||
break;
|
||||
case 'progress':
|
||||
if (isset($args['complete']) && $args['complete']) {
|
||||
$this->progress->finish();
|
||||
} else {
|
||||
$this->progress->advance();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user