mirror of
https://github.com/getgrav/grav.git
synced 2025-12-05 15:29:57 +01:00
Compare commits
8 Commits
b7e1958a6e
...
1.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fae70e5fc9 | ||
|
|
9d9247a32f | ||
|
|
94d85cd873 | ||
|
|
0f879bd1d4 | ||
|
|
fd828d452e | ||
|
|
63bbc1cac6 | ||
|
|
528032b11a | ||
|
|
a4c3a3af6d |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,3 +1,28 @@
|
||||
# v1.8.0-beta.28
|
||||
## 12/03/2025
|
||||
|
||||
1. [](#bugfix)
|
||||
* Fixed backup restriction preventing backups on systems with Grav installed under `/var/www` - Fixes [#4002](https://github.com/getgrav/grav/issues/4002)
|
||||
* Fixed XSS false positives for legitimate HTML tags containing 'on' (caption, button, section) - Fixes [grav-plugin-admin#2472](https://github.com/getgrav/grav-plugin-admin/issues/2472)
|
||||
|
||||
# v1.8.0-beta.27
|
||||
## 11/30/2025
|
||||
|
||||
1. [](#improved)
|
||||
* Hardened Twig sandbox with expanded blacklist blocking 150+ dangerous functions and attack patterns
|
||||
* Added static regex caching in Security class for improved performance
|
||||
* Added path traversal protection to backup root configuration
|
||||
* Added validation for language codes to prevent regex injection DoS
|
||||
1. [](#bugfix)
|
||||
* Fixed path traversal vulnerability in username during account creation
|
||||
* Fixed username uniqueness bypass allowing duplicate accounts
|
||||
* Fixed arbitrary file read via `read_file()` Twig function
|
||||
* Fixed DoS via malformed cron expressions in scheduler
|
||||
* Fixed password hash exposure to frontend via JSON serialization
|
||||
* Fixed email disclosure in user edit page title
|
||||
* Fixed XSS via `isindex` tag bypass (CVE-2023-31506)
|
||||
* Fixed issue with FlexObjects caching [flex-objects#187](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/187)
|
||||
|
||||
# v1.8.0-beta.26
|
||||
## 11/29/2025
|
||||
|
||||
|
||||
@@ -122,4 +122,13 @@ form:
|
||||
default: '* 3 * * *'
|
||||
validate:
|
||||
required: true
|
||||
.schedule_environment:
|
||||
type: select
|
||||
label: PLUGIN_ADMIN.BACKUPS_PROFILE_ENVIRONMENT
|
||||
help: PLUGIN_ADMIN.BACKUPS_PROFILE_ENVIRONMENT_HELP
|
||||
default: ''
|
||||
options:
|
||||
'': 'Default (cli)'
|
||||
localhost: 'Localhost'
|
||||
cli: 'CLI'
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ profiles:
|
||||
root: '/'
|
||||
schedule: false
|
||||
schedule_at: '0 3 * * *'
|
||||
schedule_environment: ''
|
||||
exclude_paths: "/backup\r\n/cache\r\n/images\r\n/logs\r\n/tmp"
|
||||
exclude_files: ".DS_Store\r\n.git\r\n.svn\r\n.hg\r\n.idea\r\n.vscode\r\nnode_modules"
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ xss_dangerous_tags:
|
||||
- bgsound
|
||||
- title
|
||||
- base
|
||||
- isindex
|
||||
uploads_dangerous_extensions:
|
||||
- php
|
||||
- php2
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
// Some standard defines
|
||||
define('GRAV', true);
|
||||
define('GRAV_VERSION', '1.8.0-beta.26');
|
||||
define('GRAV_VERSION', '1.8.0-beta.27');
|
||||
define('GRAV_SCHEMA', '1.8.0_2025-09-21_0');
|
||||
define('GRAV_TESTING', true);
|
||||
|
||||
|
||||
@@ -89,8 +89,9 @@ class Backups
|
||||
$at = $profile['schedule_at'];
|
||||
$name = $inflector::hyphenize($profile['name']);
|
||||
$logs = 'logs/backup-' . $name . '.out';
|
||||
$environment = $profile['schedule_environment'] ?? null;
|
||||
/** @var Job $job */
|
||||
$job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id], $name);
|
||||
$job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id, null, $environment], $name);
|
||||
$job->at($at);
|
||||
$job->output($logs);
|
||||
$job->backlink('/tools/backups');
|
||||
@@ -192,12 +193,19 @@ class Backups
|
||||
*
|
||||
* @param int $id
|
||||
* @param callable|null $status
|
||||
* @param string|null $environment Optional environment to load config from
|
||||
* @return string|null
|
||||
*/
|
||||
public static function backup($id = 0, ?callable $status = null)
|
||||
public static function backup($id = 0, ?callable $status = null, ?string $environment = null)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
// If environment is specified and different from current, reload config
|
||||
if ($environment && $environment !== $grav['config']->get('setup.environment')) {
|
||||
$grav->setup($environment);
|
||||
$grav['config']->reload();
|
||||
}
|
||||
|
||||
$profiles = static::getBackupProfiles();
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
@@ -233,12 +241,17 @@ class Backups
|
||||
throw new RuntimeException("Invalid backup location: {$backup_root}");
|
||||
}
|
||||
|
||||
// Ensure the backup root is within GRAV_ROOT or a parent thereof (for backing up GRAV itself)
|
||||
// Block access to system directories outside the web root
|
||||
$blockedPaths = ['/etc', '/root', '/home', '/var', '/usr', '/bin', '/sbin', '/tmp', '/proc', '/sys', '/dev'];
|
||||
foreach ($blockedPaths as $blocked) {
|
||||
if (strpos($realBackupRoot, $blocked) === 0) {
|
||||
throw new RuntimeException("Backup location not allowed: {$backup_root}");
|
||||
// Check if backup root is within GRAV_ROOT
|
||||
$isWithinGravRoot = strpos($realBackupRoot, $realGravRoot) === 0;
|
||||
|
||||
// Only apply blocklist to paths outside GRAV_ROOT to prevent backing up system directories
|
||||
// This allows backups within Grav installations under /var/www while still blocking /var/log, etc.
|
||||
if (!$isWithinGravRoot) {
|
||||
$blockedPaths = ['/etc', '/root', '/home', '/var', '/usr', '/bin', '/sbin', '/tmp', '/proc', '/sys', '/dev'];
|
||||
foreach ($blockedPaths as $blocked) {
|
||||
if (strpos($realBackupRoot, $blocked) === 0) {
|
||||
throw new RuntimeException("Backup location not allowed: {$backup_root}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -224,8 +224,9 @@ class Security
|
||||
|
||||
// Set the patterns we'll test against
|
||||
$patterns = [
|
||||
// Match any attribute starting with "on" or xmlns
|
||||
'on_events' => '#(<[^>]+[a-z\x00-\x20\"\'\/])(on[a-z]+|xmlns)\s*=[\s|\'\"].*[\s|\'\"]>#iUu',
|
||||
// Match any attribute starting with "on" or xmlns (must be preceded by whitespace/special chars)
|
||||
// Allow optional whitespace between 'on' and event name to catch obfuscation attempts
|
||||
'on_events' => '#(<[^>]+[\s\x00-\x20\"\'\/])(on\s*[a-z]+|xmlns)\s*=[\s|\'\"].*[\s|\'\"]>#iUu',
|
||||
|
||||
// Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols
|
||||
'invalid_protocols' => '#(' . implode('|', array_map('preg_quote', $invalid_protocols, ['#'])) . ')(:|\&\#58)\S.*?#iUu',
|
||||
@@ -243,8 +244,16 @@ class Security
|
||||
// Iterate over rules and return label if fail
|
||||
foreach ($patterns as $name => $regex) {
|
||||
if (!empty($enabled_rules[$name])) {
|
||||
if (preg_match($regex, (string) $string) || preg_match($regex, (string) $stripped) || preg_match($regex, $orig)) {
|
||||
return $name;
|
||||
// Skip testing 'on_events' against stripped version to avoid false positives
|
||||
// with tags like <caption>, <button>, <section> that end with 'on' or contain 'on'
|
||||
if ($name === 'on_events') {
|
||||
if (preg_match($regex, (string) $string) || preg_match($regex, $orig)) {
|
||||
return $name;
|
||||
}
|
||||
} else {
|
||||
if (preg_match($regex, (string) $string) || preg_match($regex, (string) $stripped) || preg_match($regex, $orig)) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,6 +479,29 @@ class FlexDirectory implements FlexDirectoryInterface
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a storage key for use as a cache key.
|
||||
* Symfony cache reserves characters: {}()/\@:
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function encodeCacheKey(string $key): string
|
||||
{
|
||||
return str_replace(['/', '\\', '@', ':'], ['__SLASH__', '__BSLASH__', '__AT__', '__COLON__'], $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a cache key back to the original storage key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function decodeCacheKey(string $key): string
|
||||
{
|
||||
return str_replace(['__SLASH__', '__BSLASH__', '__AT__', '__COLON__'], ['/', '\\', '@', ':'], $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
@@ -720,7 +743,12 @@ class FlexDirectory implements FlexDirectoryInterface
|
||||
//$debugger->addMessage(sprintf('Flex: Caching %d %s', \count($entries), $this->type), 'debug');
|
||||
}
|
||||
try {
|
||||
$cache->setMultiple($updated);
|
||||
// Encode storage keys for cache compatibility (Symfony cache reserves certain characters)
|
||||
$encodedUpdated = [];
|
||||
foreach ($updated as $key => $value) {
|
||||
$encodedUpdated[$this->encodeCacheKey($key)] = $value;
|
||||
}
|
||||
$cache->setMultiple($encodedUpdated);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
// TODO: log about the issue.
|
||||
@@ -752,7 +780,15 @@ class FlexDirectory implements FlexDirectoryInterface
|
||||
|
||||
$debugger->startTimer('flex-objects', sprintf('Flex: Loading %d %s', $loading, $this->type));
|
||||
|
||||
$fetched = (array)$cache->getMultiple($fetch);
|
||||
// Encode storage keys for cache compatibility (Symfony cache reserves certain characters)
|
||||
$encodedFetch = array_map([$this, 'encodeCacheKey'], $fetch);
|
||||
$encodedFetched = (array)$cache->getMultiple($encodedFetch);
|
||||
|
||||
// Decode the keys back to original storage keys
|
||||
foreach ($encodedFetched as $encodedKey => $value) {
|
||||
$fetched[$this->decodeCacheKey($encodedKey)] = $value;
|
||||
}
|
||||
|
||||
if ($fetched) {
|
||||
$index = $this->loadIndex('storage_key');
|
||||
|
||||
|
||||
@@ -1,242 +1,31 @@
|
||||
absolute_urls: false
|
||||
timezone: null
|
||||
param_sep: ':'
|
||||
wrapped_site: false
|
||||
reverse_proxy_setup: false
|
||||
force_ssl: false
|
||||
force_lowercase_urls: true
|
||||
custom_base_url: null
|
||||
username_regex: '^[a-z0-9_-]{3,16}$'
|
||||
pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}'
|
||||
intl_enabled: true
|
||||
http_x_forwarded:
|
||||
protocol: true
|
||||
host: false
|
||||
port: true
|
||||
ip: true
|
||||
languages:
|
||||
supported: null
|
||||
default_lang: null
|
||||
include_default_lang: true
|
||||
include_default_lang_file_extension: true
|
||||
translations: true
|
||||
translations_fallback: true
|
||||
session_store_active: false
|
||||
http_accept_language: false
|
||||
override_locale: false
|
||||
pages_fallback_only: false
|
||||
debug: false
|
||||
home:
|
||||
alias: /home
|
||||
hide_in_urls: false
|
||||
alias: '/home'
|
||||
|
||||
pages:
|
||||
type: regular
|
||||
dirs:
|
||||
- 'page://'
|
||||
theme: quark
|
||||
order:
|
||||
by: default
|
||||
dir: asc
|
||||
list:
|
||||
count: 20
|
||||
dateformat:
|
||||
default: null
|
||||
short: 'jS M Y'
|
||||
long: 'F jS \a\t g:ia'
|
||||
publish_dates: true
|
||||
process:
|
||||
markdown: true
|
||||
twig: false
|
||||
twig_first: false
|
||||
never_cache_twig: false
|
||||
events:
|
||||
page: true
|
||||
twig: true
|
||||
markdown:
|
||||
extra: false
|
||||
auto_line_breaks: false
|
||||
auto_url_links: false
|
||||
escape_markup: false
|
||||
special_chars:
|
||||
'>': gt
|
||||
'<': lt
|
||||
valid_link_attributes:
|
||||
- rel
|
||||
- target
|
||||
- id
|
||||
- class
|
||||
- classes
|
||||
types:
|
||||
- html
|
||||
- htm
|
||||
- xml
|
||||
- txt
|
||||
- json
|
||||
- rss
|
||||
- atom
|
||||
append_url_extension: null
|
||||
expires: 604800
|
||||
cache_control: null
|
||||
last_modified: false
|
||||
etag: true
|
||||
vary_accept_encoding: false
|
||||
redirect_default_code: '302'
|
||||
redirect_trailing_slash: 1
|
||||
redirect_default_route: 0
|
||||
ignore_files:
|
||||
- .DS_Store
|
||||
ignore_folders:
|
||||
- .git
|
||||
- .idea
|
||||
ignore_hidden: true
|
||||
hide_empty_folders: false
|
||||
url_taxonomy_filters: true
|
||||
frontmatter:
|
||||
process_twig: false
|
||||
ignore_fields:
|
||||
- form
|
||||
- forms
|
||||
|
||||
cache:
|
||||
enabled: true
|
||||
check:
|
||||
method: file
|
||||
driver: auto
|
||||
prefix: g
|
||||
purge_at: '0 4 * * *'
|
||||
clear_at: '0 3 * * *'
|
||||
clear_job_type: standard
|
||||
clear_images_by_default: false
|
||||
cli_compatibility: false
|
||||
lifetime: 604800
|
||||
purge_max_age_days: 30
|
||||
gzip: false
|
||||
allow_webserver_gzip: false
|
||||
redis:
|
||||
socket: '0'
|
||||
password: null
|
||||
database: null
|
||||
server: null
|
||||
port: null
|
||||
memcache:
|
||||
server: null
|
||||
port: null
|
||||
memcached:
|
||||
server: null
|
||||
port: null
|
||||
twig:
|
||||
cache: true
|
||||
debug: true
|
||||
auto_reload: true
|
||||
autoescape: true
|
||||
undefined_functions: true
|
||||
undefined_filters: true
|
||||
safe_functions: { }
|
||||
safe_filters: { }
|
||||
umask_fix: false
|
||||
|
||||
assets:
|
||||
css_pipeline: false
|
||||
css_pipeline_include_externals: true
|
||||
css_pipeline_before_excludes: true
|
||||
css_minify: true
|
||||
css_minify_windows: false
|
||||
css_rewrite: true
|
||||
js_pipeline: false
|
||||
js_pipeline_include_externals: true
|
||||
js_pipeline_before_excludes: true
|
||||
js_module_pipeline: false
|
||||
js_module_pipeline_include_externals: true
|
||||
js_module_pipeline_before_excludes: true
|
||||
js_minify: true
|
||||
enable_asset_timestamp: false
|
||||
enable_asset_sri: false
|
||||
collections:
|
||||
jquery: 'system://assets/jquery/jquery-3.x.min.js'
|
||||
|
||||
errors:
|
||||
display: 1
|
||||
display: true
|
||||
log: true
|
||||
log:
|
||||
handler: file
|
||||
syslog:
|
||||
facility: local6
|
||||
tag: grav
|
||||
|
||||
debugger:
|
||||
enabled: false
|
||||
provider: clockwork
|
||||
censored: false
|
||||
shutdown:
|
||||
close_connection: true
|
||||
twig: true
|
||||
images:
|
||||
adapter: gd
|
||||
default_image_quality: 85
|
||||
cache_all: false
|
||||
cache_perms: '0755'
|
||||
debug: false
|
||||
auto_fix_orientation: true
|
||||
seofriendly: false
|
||||
cls:
|
||||
auto_sizes: false
|
||||
aspect_ratio: false
|
||||
retina_scale: '1'
|
||||
defaults:
|
||||
loading: auto
|
||||
decoding: auto
|
||||
fetchpriority: auto
|
||||
watermark:
|
||||
image: 'system://images/watermark.png'
|
||||
position_y: center
|
||||
position_x: center
|
||||
scale: 33
|
||||
watermark_all: false
|
||||
media:
|
||||
enable_media_timestamp: false
|
||||
unsupported_inline_types: null
|
||||
allowed_fallback_types: null
|
||||
auto_metadata_exif: false
|
||||
upload_limit: 2097152
|
||||
session:
|
||||
enabled: true
|
||||
initialize: true
|
||||
timeout: 1800
|
||||
name: grav-site
|
||||
uniqueness: path
|
||||
secure: false
|
||||
secure_https: true
|
||||
httponly: true
|
||||
samesite: Lax
|
||||
split: true
|
||||
domain: null
|
||||
path: null
|
||||
|
||||
gpm:
|
||||
releases: testing
|
||||
official_gpm_only: true
|
||||
verify_peer: true
|
||||
|
||||
updates:
|
||||
safe_upgrade: true
|
||||
http:
|
||||
method: auto
|
||||
enable_proxy: true
|
||||
proxy_url: null
|
||||
proxy_cert_path: null
|
||||
concurrent_connections: 5
|
||||
verify_peer: true
|
||||
verify_host: true
|
||||
accounts:
|
||||
type: regular
|
||||
storage: file
|
||||
avatar: gravatar
|
||||
flex:
|
||||
cache:
|
||||
index:
|
||||
enabled: true
|
||||
lifetime: 60
|
||||
object:
|
||||
enabled: true
|
||||
lifetime: 600
|
||||
render:
|
||||
enabled: true
|
||||
lifetime: 600
|
||||
strict_mode:
|
||||
yaml_compat: false
|
||||
twig_compat: false
|
||||
blueprint_compat: false
|
||||
safe_upgrade_snapshot_limit: 5
|
||||
|
||||
Reference in New Issue
Block a user