mirror of
https://github.com/getgrav/grav.git
synced 2026-03-01 10:01:26 +01:00
Merge branch '1.7' of github.com:getgrav/grav into 1.7
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
* Allow `JsonFormatter` options to be passed as a string
|
||||
* Hide Flex Pages frontend configuration (not ready for production use)
|
||||
* Improve Flex configuration: gather views together in blueprint
|
||||
* Added XSS detection to all forms (use `check_xss: false` to disable it per field)
|
||||
1. [](#bugfix)
|
||||
* *Menu Visibility Requires Access* Security option setting wrong frontmatter [login#265](https://github.com/getgrav/grav-plugin-login/issues/265)
|
||||
* Accessing page with unsupported file extension (jpg, pdf, xsl) will use wrong mime type [#3031](https://github.com/getgrav/grav/issues/3031)
|
||||
@@ -30,6 +31,7 @@
|
||||
* Fixed fatal error in `CompiledFile` if the cached version is broken
|
||||
* Fixed updated media missing from media when editing Flex Object after page reload
|
||||
* Fixed issue with `config-default@` breaking on set [#1972](https://github.com/getgrav/grav-plugin-admin/issues/1971)
|
||||
* Escape titles in Flex pages list [flex-objects#84](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/84)
|
||||
|
||||
# v1.7.0-rc.17
|
||||
## 10/07/2020
|
||||
|
||||
@@ -39,12 +39,13 @@ form:
|
||||
.command:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.COMMAND
|
||||
placeholder: 'cd ~;ls -lah;'
|
||||
placeholder: 'ls'
|
||||
validate:
|
||||
required: true
|
||||
.args:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
|
||||
placeholder: '-lah'
|
||||
.at:
|
||||
type: cron
|
||||
label: PLUGIN_ADMIN.SCHEDULER_RUNAT
|
||||
|
||||
@@ -1343,6 +1343,12 @@ form:
|
||||
label: PLUGIN_ADMIN.SESSION_PATH
|
||||
help: PLUGIN_ADMIN.SESSION_PATH_HELP
|
||||
|
||||
session.samesite:
|
||||
type: text
|
||||
size: small
|
||||
label: PLUGIN_ADMIN.SESSION_SAMESITE
|
||||
help: PLUGIN_ADMIN.SESSION_SAMESITE_HELP
|
||||
|
||||
session.split:
|
||||
type: toggle
|
||||
label: PLUGIN_ADMIN.SESSION_SPLIT
|
||||
|
||||
@@ -168,6 +168,7 @@ session:
|
||||
uniqueness: path # Should sessions be `path` based or `security.salt` based
|
||||
secure: false # Set session secure. If true, indicates that communication for this cookie must be over an encrypted transmission. Enable this only on sites that run exclusively on HTTPS
|
||||
httponly: true # Set session HTTP only. If true, indicates that cookies should be used only over HTTP, and JavaScript modification is not allowed.
|
||||
samesite: # Set session SameSite. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
||||
split: true # Sessions should be independent between site and plugins (such as admin)
|
||||
path:
|
||||
|
||||
|
||||
@@ -94,9 +94,10 @@ GRAV:
|
||||
YR_PLURAL: yrs
|
||||
DEC_PLURAL: decs
|
||||
FORM:
|
||||
VALIDATION_FAIL: <b>Validation failed:</b>
|
||||
INVALID_INPUT: Invalid input in
|
||||
MISSING_REQUIRED_FIELD: Missing required field:
|
||||
VALIDATION_FAIL: '<b>Validation failed:</b>'
|
||||
INVALID_INPUT: 'Invalid input in'
|
||||
MISSING_REQUIRED_FIELD: 'Missing required field:'
|
||||
XSS_ISSUES: "Potential XSS issues detected in '%s' field"
|
||||
MONTHS_OF_THE_YEAR: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
||||
DAYS_OF_THE_WEEK: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
||||
YES: "Yes"
|
||||
|
||||
@@ -260,14 +260,15 @@ class Blueprint extends BlueprintForm
|
||||
* Validate data against blueprints.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $options
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function validate(array $data)
|
||||
public function validate(array $data, array $options = [])
|
||||
{
|
||||
$this->initInternals();
|
||||
|
||||
$this->blueprintSchema->validate($data);
|
||||
$this->blueprintSchema->validate($data, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,9 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
{
|
||||
use Export;
|
||||
|
||||
/** @var array */
|
||||
protected $filter = ['validation' => true, 'check_xss' => true];
|
||||
|
||||
/** @var array */
|
||||
protected $ignoreFormKeys = [
|
||||
'title' => true,
|
||||
@@ -57,13 +60,14 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
* Validate data against blueprints.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $options
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function validate(array $data)
|
||||
public function validate(array $data, array $options = [])
|
||||
{
|
||||
try {
|
||||
$validation = $this->items['']['form']['validation'] ?? 'loose';
|
||||
$messages = $this->validateArray($data, $this->nested, $validation === 'strict');
|
||||
$messages = $this->validateArray($data, $this->nested, $validation === 'strict', $options['check_xss'] ?? true);
|
||||
} catch (RuntimeException $e) {
|
||||
throw (new ValidationException($e->getMessage(), $e->getCode(), $e))->setMessages();
|
||||
}
|
||||
@@ -141,16 +145,18 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
* @param array $data
|
||||
* @param array $rules
|
||||
* @param bool $strict
|
||||
* @param bool $xss
|
||||
* @return array
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function validateArray(array $data, array $rules, bool $strict)
|
||||
protected function validateArray(array $data, array $rules, bool $strict, bool $xss = true)
|
||||
{
|
||||
$messages = $this->checkRequired($data, $rules);
|
||||
|
||||
foreach ($data as $key => $child) {
|
||||
$val = $rules[$key] ?? $rules['*'] ?? null;
|
||||
$rule = is_string($val) ? $this->items[$val] : null;
|
||||
$checkXss = $xss;
|
||||
|
||||
if ($rule) {
|
||||
// Item has been defined in blueprints.
|
||||
@@ -160,11 +166,14 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
}
|
||||
|
||||
$messages += Validation::validate($child, $rule);
|
||||
|
||||
} elseif (is_array($child) && is_array($val)) {
|
||||
// Array has been defined in blueprints.
|
||||
$messages += $this->validateArray($child, $val, $strict);
|
||||
$checkXss = false;
|
||||
|
||||
} elseif ($strict) {
|
||||
// Undefined/extra item.
|
||||
// Undefined/extra item in strict mode.
|
||||
/** @var Config $config */
|
||||
$config = Grav::instance()['config'];
|
||||
if (!$config->get('system.strict_mode.blueprint_strict_compat', true)) {
|
||||
@@ -173,6 +182,10 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|
||||
|
||||
user_error(sprintf('Having extra key %s in your data is deprecated with blueprint having \'validation: strict\'', $key), E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
if ($checkXss) {
|
||||
$messages += Validation::checkSafety($child, $rule ?: ['name' => $key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $messages;
|
||||
|
||||
@@ -13,8 +13,12 @@ use ArrayAccess;
|
||||
use Countable;
|
||||
use DateTime;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Language\Language;
|
||||
use Grav\Common\Security;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Common\Yaml;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Traversable;
|
||||
use function count;
|
||||
use function is_array;
|
||||
@@ -92,6 +96,72 @@ class Validation
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param array $field
|
||||
*/
|
||||
public static function checkSafety($value, array $field)
|
||||
{
|
||||
$messages = [];
|
||||
|
||||
$type = $field['validate']['type'] ?? $field['type'] ?? 'text';
|
||||
if ($type === 'unset' || !($field['check_xss'] ?? true)) {
|
||||
return $messages;
|
||||
}
|
||||
$name = ucfirst($field['label'] ?? $field['name'] ?? 'UNKNOWN');
|
||||
|
||||
$user = Grav::instance()['user'] ?? null;
|
||||
$xss_whitelist = Grav::instance()['config']->get('security.xss_whitelist', 'admin.super');
|
||||
|
||||
// Get language class.
|
||||
/** @var Language $language */
|
||||
$language = Grav::instance()['language'];
|
||||
|
||||
if (!static::authorize($xss_whitelist, $user)) {
|
||||
if (is_string($value)) {
|
||||
$violation = Security::detectXss($value);
|
||||
if ($violation) {
|
||||
$messages[$name][] = $language->translate(['GRAV.FORM.XSS_ISSUES', $language->translate($name)], null, true);
|
||||
}
|
||||
} elseif (is_array($value)) {
|
||||
$violations = Security::detectXssFromArray($value, $name);
|
||||
if ($violations) {
|
||||
$messages[$name][] = $language->translate(['GRAV.FORM.XSS_ISSUES', $language->translate($name)], null, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks user authorisation to the action.
|
||||
*
|
||||
* @param string|string[] $action
|
||||
* @param UserInterface|null $user
|
||||
* @return bool
|
||||
*/
|
||||
public static function authorize($action, UserInterface $user = null)
|
||||
{
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$action = (array)$action;
|
||||
foreach ($action as $a) {
|
||||
// Ignore 'admin.super' if it's not the only value to be checked.
|
||||
if ($a === 'admin.super' && count($action) > 1 && $user instanceof FlexObjectInterface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($user->authorize($a)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter value against a blueprint field definition.
|
||||
*
|
||||
|
||||
@@ -489,7 +489,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
|
||||
$payload = [
|
||||
'item-key' => basename($child->rawRoute() ?? $child->getKey()),
|
||||
'icon' => $icon,
|
||||
'title' => $child->menu(),
|
||||
'title' => htmlspecialchars($child->menu()),
|
||||
'route' => [
|
||||
'display' => $child->getRoute()->toString(false) ?: '/',
|
||||
'raw' => $child->rawRoute(),
|
||||
|
||||
@@ -1079,7 +1079,7 @@ class Pages
|
||||
$option = $current->route();
|
||||
} else {
|
||||
$extra = $showSlug ? '(' . $current->slug() . ') ' : '';
|
||||
$option = str_repeat('—-', $level). '▸ ' . $extra . $current->title();
|
||||
$option = str_repeat('—-', $level). '▸ ' . $extra . htmlspecialchars($current->title());
|
||||
}
|
||||
|
||||
$list[$route] = $option;
|
||||
|
||||
@@ -298,7 +298,7 @@ class Scheduler
|
||||
*/
|
||||
public function isCrontabSetup()
|
||||
{
|
||||
$process = new Process('crontab -l');
|
||||
$process = new Process(['crontab', '-l']);
|
||||
$process->run();
|
||||
|
||||
if ($process->isSuccessful()) {
|
||||
|
||||
@@ -44,6 +44,7 @@ class SessionServiceProvider implements ServiceProviderInterface
|
||||
$cookie_httponly = (bool)$config->get('system.session.httponly', true);
|
||||
$cookie_lifetime = (int)$config->get('system.session.timeout', 1800);
|
||||
$cookie_path = $config->get('system.session.path');
|
||||
$cookie_samesite = $config->get('system.session.samesite');
|
||||
if (null === $cookie_path) {
|
||||
$cookie_path = '/' . trim(Uri::filterPath($uri->rootUrl(false)), '/');
|
||||
}
|
||||
@@ -95,7 +96,8 @@ class SessionServiceProvider implements ServiceProviderInterface
|
||||
'cookie_path' => $cookie_path,
|
||||
'cookie_domain' => $cookie_domain,
|
||||
'cookie_secure' => $cookie_secure,
|
||||
'cookie_httponly' => $cookie_httponly
|
||||
'cookie_httponly' => $cookie_httponly,
|
||||
'cookie_samesite' => $cookie_samesite
|
||||
] + (array) $config->get('system.session.options');
|
||||
|
||||
$session = new Session($options);
|
||||
|
||||
@@ -928,11 +928,7 @@ abstract class Utils
|
||||
public static function checkFilename($filename)
|
||||
{
|
||||
$dangerous_extensions = Grav::instance()['config']->get('security.uploads_dangerous_extensions', []);
|
||||
array_walk($dangerous_extensions, function (&$val) {
|
||||
$val = '.' . $val;
|
||||
});
|
||||
|
||||
$extension = '.' . pathinfo($filename, PATHINFO_EXTENSION);
|
||||
$extension = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
|
||||
return !(
|
||||
// Empty filenames are not allowed.
|
||||
@@ -941,8 +937,8 @@ abstract class Utils
|
||||
|| strtr($filename, "\t\v\n\r\0\\/", '_______') !== $filename
|
||||
// Filename should not start or end with dot or space.
|
||||
|| trim($filename, '. ') !== $filename
|
||||
// Filename should not contain .php in it.
|
||||
|| static::contains($extension, $dangerous_extensions)
|
||||
// File extension should not be part of configured dangerous extensions
|
||||
|| in_array($extension, $dangerous_extensions)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
|
||||
if ($validate) {
|
||||
$blueprint = $this->getFlexDirectory()->getBlueprint();
|
||||
|
||||
$blueprint->validate($elements);
|
||||
$blueprint->validate($elements, ['check_xss' => false]);
|
||||
|
||||
$elements = $blueprint->filter($elements, true, true);
|
||||
}
|
||||
@@ -576,7 +576,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
|
||||
$test = $blueprint->mergeData($elements, $data);
|
||||
|
||||
// Validate and filter elements and throw an error if any issues were found.
|
||||
$blueprint->validate($test + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()]);
|
||||
$blueprint->validate($test + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()], ['check_xss' => false]);
|
||||
$data = $blueprint->filter($data, true, true);
|
||||
|
||||
// Finally update the object.
|
||||
|
||||
@@ -140,6 +140,7 @@ class Session implements SessionInterface
|
||||
'use_strict_mode' => true,
|
||||
'use_cookies' => true,
|
||||
'use_only_cookies' => true,
|
||||
'cookie_samesite' => true,
|
||||
'referer_check' => true,
|
||||
'cache_limiter' => true,
|
||||
'cache_expire' => true,
|
||||
@@ -243,14 +244,19 @@ class Session implements SessionInterface
|
||||
if ($sessionExists) {
|
||||
$params = session_get_cookie_params();
|
||||
|
||||
$cookie_options = array (
|
||||
'expires' => time() + $params['lifetime'],
|
||||
'path' => $params['path'],
|
||||
'domain' => $params['domain'],
|
||||
'secure' => $params['secure'],
|
||||
'httponly' => $params['httponly'],
|
||||
'samesite' => $params['samesite']
|
||||
);
|
||||
|
||||
setcookie(
|
||||
$sessionName,
|
||||
session_id(),
|
||||
time() + $params['lifetime'],
|
||||
$params['path'],
|
||||
$params['domain'],
|
||||
$params['secure'],
|
||||
$params['httponly']
|
||||
$cookie_options
|
||||
);
|
||||
}
|
||||
|
||||
@@ -309,14 +315,20 @@ class Session implements SessionInterface
|
||||
public function invalidate()
|
||||
{
|
||||
$params = session_get_cookie_params();
|
||||
|
||||
$cookie_options = array (
|
||||
'expires' => time() - 42000,
|
||||
'path' => $params['path'],
|
||||
'domain' => $params['domain'],
|
||||
'secure' => $params['secure'],
|
||||
'httponly' => $params['httponly'],
|
||||
'samesite' => $params['samesite']
|
||||
);
|
||||
|
||||
setcookie(
|
||||
session_name(),
|
||||
'',
|
||||
time() - 42000,
|
||||
$params['path'],
|
||||
$params['domain'],
|
||||
$params['secure'],
|
||||
$params['httponly']
|
||||
$cookie_options
|
||||
);
|
||||
|
||||
if ($this->isSessionStarted()) {
|
||||
|
||||
@@ -540,4 +540,20 @@ class UtilsTest extends \Codeception\TestCase\Test
|
||||
$this->assertSame('//foo.com', Utils::url('//foo.com'));
|
||||
$this->assertSame('//foo.com?param=x', Utils::url('//foo.com?param=x'));
|
||||
}
|
||||
|
||||
public function testCheckFilename()
|
||||
{
|
||||
// configure extension for consistent results
|
||||
/** @var \Grav\Common\Config\Config $config */
|
||||
$config = $this->grav['config'];
|
||||
$config->set('security.uploads_dangerous_extensions', ['php', 'html', 'htm', 'exe', 'js']);
|
||||
|
||||
$this->assertFalse(Utils::checkFilename('foo.php'));
|
||||
$this->assertFalse(Utils::checkFilename('bar.js'));
|
||||
|
||||
$this->assertTrue(Utils::checkFilename('foo.json'));
|
||||
$this->assertTrue(Utils::checkFilename('foo.xml'));
|
||||
$this->assertTrue(Utils::checkFilename('foo.yaml'));
|
||||
$this->assertTrue(Utils::checkFilename('foo.yml'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user