Compare commits

...

8 Commits

Author SHA1 Message Date
Andy Miller
fae70e5fc9 fixes #4002 - Backups blocking /var/www
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-12-03 19:30:32 -07:00
Andy Miller
9d9247a32f fix false positives in Security with on_events
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-12-03 14:17:17 -07:00
Andy Miller
94d85cd873 add support for environment in grav scheduler
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-12-03 10:41:29 -07:00
Andy Miller
0f879bd1d4 prepare for beta.27 release
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-30 16:17:37 -07:00
Andy Miller
fd828d452e trim down default user/config/system.yaml
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-30 16:14:35 -07:00
Andy Miller
63bbc1cac6 flex-objects caching fix
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-30 16:06:31 -07:00
Andy Miller
528032b11a update changelog
Signed-off-by: Andy Miller <rhuk@mac.com>
2025-11-29 21:18:57 -07:00
Andy Miller
a4c3a3af6d Add isindex to XSS dangerous tags (CVE-2023-31506 / GHSA-h85h-xm8x-vfw7)
The original CVE-2023-31506 fix missed the deprecated <isindex> HTML tag,
which can still be used for XSS via event handlers like onmouseover.

The <isindex> tag is deprecated in HTML5 and has no legitimate modern use.
2025-11-29 21:07:23 -07:00
9 changed files with 119 additions and 236 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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"

View File

@@ -31,6 +31,7 @@ xss_dangerous_tags:
- bgsound
- title
- base
- isindex
uploads_dangerous_extensions:
- php
- php2

View File

@@ -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);

View File

@@ -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}");
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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');

View File

@@ -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