From 932b6847872259b7b6d63d7ebd35ba3fc9145f7d Mon Sep 17 00:00:00 2001 From: Msprg Date: Mon, 9 Feb 2026 15:46:17 +0000 Subject: [PATCH 1/9] Add configurable history username env injection for synced user keys --- config/config.ini.example | 9 +++ migrations/010.php | 8 ++ model/migrationdirectory.php | 2 +- model/server.php | 4 +- public_html/extra.js | 3 + scripts/sync.php | 142 +++++++++++++++++++++++++++++------ templates/server.php | 86 +++++++++++++++++---- views/server.php | 13 ++++ 8 files changed, 228 insertions(+), 39 deletions(-) create mode 100644 migrations/010.php diff --git a/config/config.ini.example b/config/config.ini.example index 9ddfd9c..7eea6ba 100644 --- a/config/config.ini.example +++ b/config/config.ini.example @@ -79,6 +79,15 @@ hostname_verification = 0 ; 2: Only a link to the public key web page will be added to each key comment_key_files = 1 +; Add environment="" to synced user key lines. +; 1: enabled by default (can be overridden per server) +; 0: disabled by default (can be overridden per server) +history_username_env_default = 1 + +; Format for the environment value. +; Supported placeholder: {uid} (required, otherwise a safe default is used) +history_username_env_format = BASH_HISTORY_USERNAME={uid} + [monitoring] ; Status information is stored in the following file on target machines: status_file_path = "/var/local/keys-sync.status" diff --git a/migrations/010.php b/migrations/010.php new file mode 100644 index 0000000..e35b71f --- /dev/null +++ b/migrations/010.php @@ -0,0 +1,8 @@ +database->query(" +ALTER TABLE `server` +ADD `history_username_env_mode` enum('inherit', 'enabled', 'disabled') NOT NULL DEFAULT 'inherit', +ADD `history_username_env_format` varchar(255) NULL; +"); diff --git a/model/migrationdirectory.php b/model/migrationdirectory.php index 5f19943..8056317 100644 --- a/model/migrationdirectory.php +++ b/model/migrationdirectory.php @@ -22,7 +22,7 @@ class MigrationDirectory extends DBDirectory { /** * Increment this constant to activate a new migration from the migrations directory */ - const LAST_MIGRATION = 9; + const LAST_MIGRATION = 10; public function __construct() { parent::__construct(); diff --git a/model/server.php b/model/server.php index dac4d93..ad7ec79 100644 --- a/model/server.php +++ b/model/server.php @@ -62,6 +62,8 @@ public function update() { case 'key_management': case 'authorization': case 'custom_keys': + case 'history_username_env_mode': + case 'history_username_env_format': $resync = true; break; case 'host_key': @@ -824,4 +826,4 @@ public function parse_jumphosts(): array { } class ServerNoteNotFoundException extends Exception {} -class AccountNameInvalid extends InvalidArgumentException {} \ No newline at end of file +class AccountNameInvalid extends InvalidArgumentException {} diff --git a/public_html/extra.js b/public_html/extra.js index faa3b74..d13acf2 100644 --- a/public_html/extra.js +++ b/public_html/extra.js @@ -151,12 +151,14 @@ $(function() { form.each(function() { $('#authorization.hide').hide().removeClass('hide'); $('#ldap_access_options.hide').hide().removeClass('hide'); + $('#history_username_env.hide').hide().removeClass('hide'); $("input[name='key_management']", form).on('click', function() {display_relevant_options()}); $("input[name='authorization']", form).on('click', function() {display_relevant_options()}); function display_relevant_options() { if($("input[name='key_management']:checked").val() == 'keys') { $('#authorization').show('fast'); $('#supervision').show('fast'); + $('#history_username_env').show('fast'); if($("input[name='authorization']:checked").val() == 'manual') { $('#ldap_access_options').hide('fast'); } else { @@ -166,6 +168,7 @@ $(function() { $('#authorization').hide('fast'); $('#ldap_access_options').hide('fast'); $('#supervision').hide('fast'); + $('#history_username_env').hide('fast'); } } diff --git a/scripts/sync.php b/scripts/sync.php index c70d626..32a66db 100755 --- a/scripts/sync.php +++ b/scripts/sync.php @@ -325,25 +325,25 @@ function sync_server($id, $only_username = null, $preview = false) { foreach($accounts as $account) { if($account->active == 0 || $account->sync_status == 'proposed') continue; $username = str_replace('/', '', $account->name); - $keyfile = sprintf($header, "account '{$account->name}'", $config['web']['baseurl']."/servers/".urlencode($hostname)."/accounts/".urlencode($account->name)); - // Collect a set of all groups that the account is a member of (directly or indirectly) and the account itself - $sets = $account->list_group_membership(); - $sets[] = $account; - foreach($sets as $set) { - if(get_class($set) == 'Group') { - if($set->active == 0) continue; // Rules for inactive groups should be ignored - if ($comment == 1) { - $keyfile .= "# === Start of rules applied due to membership in {$set->name} group ===\n"; + $keyfile = sprintf($header, "account '{$account->name}'", $config['web']['baseurl']."/servers/".urlencode($hostname)."/accounts/".urlencode($account->name)); + // Collect a set of all groups that the account is a member of (directly or indirectly) and the account itself + $sets = $account->list_group_membership(); + $sets[] = $account; + foreach($sets as $set) { + if(get_class($set) == 'Group') { + if($set->active == 0) continue; // Rules for inactive groups should be ignored + if ($comment == 1) { + $keyfile .= "# === Start of rules applied due to membership in {$set->name} group ===\n"; + } + } + $access_rules = $set->list_access(); + $keyfile .= get_keys($access_rules, $account->name, $hostname, $comment, $server); + if(get_class($set) == 'Group' && $comment == 1) { + $keyfile .= "# === End of rules applied due to membership in {$set->name} group ===\n\n"; } } - $access_rules = $set->list_access(); - $keyfile .= get_keys($access_rules, $account->name, $hostname, $comment); - if(get_class($set) == 'Group' && $comment == 1) { - $keyfile .= "# === End of rules applied due to membership in {$set->name} group ===\n\n"; - } + $keyfiles[$username] = array('keyfile' => $keyfile, 'check' => false, 'account' => $account); } - $keyfiles[$username] = array('keyfile' => $keyfile, 'check' => false, 'account' => $account); - } if($server->authorization == 'automatic LDAP' || $server->authorization == 'manual LDAP') { // Generate keyfiles for LDAP users $optiontext = array(); @@ -361,8 +361,9 @@ function sync_server($id, $only_username = null, $preview = false) { $keys = $user->list_public_keys($username, $hostname, false); if(count($keys) > 0) { if($user->active) { + $user_prefix = add_user_history_username_env_option($prefix, $user, $server); foreach($keys as $key) { - $keyfile .= $prefix.$key->export_userkey_with_fixed_comment($user, $comment)."\n"; + $keyfile .= $user_prefix.$key->export_userkey_with_fixed_comment($user, $comment)."\n"; } } elseif ($comment == 1) { $keyfile .= "# Account disabled\n"; @@ -494,7 +495,99 @@ function($reason) use ($server, $keyfiles) { echo date('c')." {$hostname}: Sync finished\n"; } -function append_user_keys($keyfile, $entity, $prefix, $account_name, $hostname, $comment, $grant_details = null) { +function get_default_history_username_env_format() { + return 'BASH_HISTORY_USERNAME={uid}'; +} + +function normalize_history_username_env_format($format) { + $format = trim((string)$format); + if($format === '' || strpos($format, '{uid}') === false) { + return get_default_history_username_env_format(); + } + return $format; +} + +function get_global_history_username_env_enabled() { + global $config; + if(!isset($config['privacy']) || !isset($config['privacy']['history_username_env_default'])) { + return true; + } + return intval($config['privacy']['history_username_env_default']) === 1; +} + +function get_global_history_username_env_format() { + global $config; + if(isset($config['privacy']) && isset($config['privacy']['history_username_env_format'])) { + return normalize_history_username_env_format($config['privacy']['history_username_env_format']); + } + return get_default_history_username_env_format(); +} + +function get_server_history_username_env_mode($server) { + try { + $mode = $server->history_username_env_mode; + } catch(Exception $e) { + return 'inherit'; + } + if($mode !== 'enabled' && $mode !== 'disabled') { + return 'inherit'; + } + return $mode; +} + +function get_server_history_username_env_enabled($server) { + $mode = get_server_history_username_env_mode($server); + switch($mode) { + case 'enabled': + return true; + case 'disabled': + return false; + default: + return get_global_history_username_env_enabled(); + } +} + +function get_server_history_username_env_format($server) { + try { + $format = trim((string)$server->history_username_env_format); + } catch(Exception $e) { + $format = ''; + } + if($format !== '') { + return normalize_history_username_env_format($format); + } + return get_global_history_username_env_format(); +} + +function escape_authorized_keys_option_value($value) { + return str_replace(array('\\', '"'), array('\\\\', '\\"'), $value); +} + +function append_authorized_keys_option($prefix, $option) { + $prefix = trim((string)$prefix); + if($prefix === '') { + return $option.' '; + } + return rtrim($prefix, ',').','.$option.' '; +} + +function get_user_history_username_env_option($user, $server) { + if(!get_server_history_username_env_enabled($server)) { + return null; + } + $value = str_replace('{uid}', $user->uid, get_server_history_username_env_format($server)); + return 'environment="'.escape_authorized_keys_option_value($value).'"'; +} + +function add_user_history_username_env_option($prefix, $user, $server) { + $option = get_user_history_username_env_option($user, $server); + if(is_null($option)) { + return $prefix; + } + return append_authorized_keys_option($prefix, $option); +} + +function append_user_keys($keyfile, $entity, $prefix, $account_name, $hostname, $comment, $server, $grant_details = null) { if ($comment == 1) { $keyfile .= "# {$entity->uid}"; if (!is_null($grant_details)) { @@ -503,6 +596,7 @@ function append_user_keys($keyfile, $entity, $prefix, $account_name, $hostname, $keyfile .= "\n"; } if($entity->active) { + $prefix = add_user_history_username_env_option($prefix, $entity, $server); $keys = $entity->list_public_keys($account_name, $hostname, false); foreach($keys as $key) { $keyfile .= $prefix.$key->export_userkey_with_fixed_comment($entity, $comment)."\n"; @@ -532,7 +626,7 @@ function append_serveraccount_keys($keyfile, $entity, $prefix, $account_name, $h return $keyfile; } -function get_keys($access_rules, $account_name, $hostname, $comment) { +function get_keys($access_rules, $account_name, $hostname, $comment, $server) { $keyfile = ''; foreach($access_rules as $access) { $grant_date = new DateTime($access->grant_date); @@ -553,6 +647,7 @@ function get_keys($access_rules, $account_name, $hostname, $comment) { $account_name, $hostname, $comment, + $server, "granted access by {$access->granted_by->uid} on {$grant_date_full}" ); break; @@ -579,7 +674,7 @@ function get_keys($access_rules, $account_name, $hostname, $comment) { if ($comment == 1) { $keyfile .= "# == Start of {$entity->name} group members ==\n"; } - $keyfile .= get_group_keys($entity->list_members(), $account_name, $hostname, $prefix, $seen, $comment); + $keyfile .= get_group_keys($entity->list_members(), $account_name, $hostname, $prefix, $seen, $comment, $server); if ($comment == 1) { $keyfile .= "# == End of {$entity->name} group members ==\n"; } @@ -592,7 +687,7 @@ function get_keys($access_rules, $account_name, $hostname, $comment) { return $keyfile; } -function get_group_keys($entities, $account_name, $hostname, $prefix, &$seen, $comment) { +function get_group_keys($entities, $account_name, $hostname, $prefix, &$seen, $comment, $server) { $keyfile = ''; foreach($entities as $entity) { switch(get_class($entity)) { @@ -603,7 +698,8 @@ function get_group_keys($entities, $account_name, $hostname, $prefix, &$seen, $c $prefix, $account_name, $hostname, - $comment + $comment, + $server ); break; case 'ServerAccount': @@ -625,7 +721,7 @@ function get_group_keys($entities, $account_name, $hostname, $prefix, &$seen, $c $keyfile .= "\n"; $keyfile .= "# == Start of {$entity->name} group members ==\n"; } - $keyfile .= get_group_keys($entity->list_members(), $account_name, $hostname, $prefix, $seen, $comment); + $keyfile .= get_group_keys($entity->list_members(), $account_name, $hostname, $prefix, $seen, $comment, $server); if ($comment == 1) { $keyfile .= "# == End of {$entity->name} group members ==\n"; } diff --git a/templates/server.php b/templates/server.php index 4dcb9cd..8650ddb 100644 --- a/templates/server.php +++ b/templates/server.php @@ -369,8 +369,8 @@ - get('ldap_access_options'); ?> -
+ get('ldap_access_options'); ?> +
@@ -387,16 +387,49 @@
-
- +
+ +
-
-
-
- + get('server')->history_username_env_mode; + if($history_username_env_mode != 'enabled' && $history_username_env_mode != 'disabled') { + $history_username_env_mode = 'inherit'; + } + $history_username_env_format = trim((string)$this->get('server')->history_username_env_format); + ?> +
+ +
+
+ +
+
+ +
+
+ +
+ + +

Supported placeholder: {uid}. If missing, sync falls back to BASH_HISTORY_USERNAME={uid}.

+
+
+
+
+ +
-
SSH port number
@@ -424,8 +457,8 @@ } ?> - get('server')->key_management == 'keys' && $this->get('server')->authorization != 'manual') { ?> -
LDAP access options
+ get('server')->key_management == 'keys' && $this->get('server')->authorization != 'manual') { ?> +
LDAP access options
-
- -
+ + + get('server')->key_management == 'keys') { ?> +
History username env
+
+ get('server')->history_username_env_mode; + $history_username_env_format = trim((string)$this->get('server')->history_username_env_format); + switch($history_username_env_mode) { + case 'enabled': + out('Force enabled'); + break; + case 'disabled': + out('Force disabled'); + break; + default: + out('Inherit global default'); + } + out(' | Format: '); + if($history_username_env_format === '') { + out('Inherit global format'); + } else { + out($history_username_env_format); + } + ?> +
+ + get('server_admin_can_reset_host_key')) { ?>
diff --git a/views/server.php b/views/server.php index 8b910a5..86afbf4 100644 --- a/views/server.php +++ b/views/server.php @@ -121,6 +121,19 @@ $server->key_management = $_POST['key_management']; $server->authorization = $_POST['authorization']; $server->key_scan = $_POST['key_scan']; + $history_username_env_mode = isset($_POST['history_username_env_mode']) ? $_POST['history_username_env_mode'] : 'inherit'; + if($history_username_env_mode !== 'inherit' && $history_username_env_mode !== 'enabled' && $history_username_env_mode !== 'disabled') { + $history_username_env_mode = 'inherit'; + } + $history_username_env_format = null; + if(isset($_POST['history_username_env_format'])) { + $history_username_env_format = trim($_POST['history_username_env_format']); + if($history_username_env_format === '') { + $history_username_env_format = null; + } + } + $server->history_username_env_mode = $history_username_env_mode; + $server->history_username_env_format = $history_username_env_format; try { $server->update(); $alert = new UserAlert; From 2d129e4ffa27dfbb1d83647afd6a46f544482e56 Mon Sep 17 00:00:00 2001 From: "Curious, aren't we?" <18015488+Msprg@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:33:22 +0100 Subject: [PATCH 2/9] Update views/server.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- views/server.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/views/server.php b/views/server.php index 86afbf4..6b9d805 100644 --- a/views/server.php +++ b/views/server.php @@ -121,15 +121,17 @@ $server->key_management = $_POST['key_management']; $server->authorization = $_POST['authorization']; $server->key_scan = $_POST['key_scan']; - $history_username_env_mode = isset($_POST['history_username_env_mode']) ? $_POST['history_username_env_mode'] : 'inherit'; - if($history_username_env_mode !== 'inherit' && $history_username_env_mode !== 'enabled' && $history_username_env_mode !== 'disabled') { - $history_username_env_mode = 'inherit'; - } $history_username_env_format = null; if(isset($_POST['history_username_env_format'])) { $history_username_env_format = trim($_POST['history_username_env_format']); if($history_username_env_format === '') { $history_username_env_format = null; + } else if(!preg_match('/^[a-zA-Z0-9_\-={}]+$/', $history_username_env_format)) { + $alert = new UserAlert; + $alert->content = "Invalid history username env format. Only alphanumeric characters, underscores, hyphens, equals signs, and curly braces are allowed."; + $alert->class = 'danger'; + $active_user->add_alert($alert); + $history_username_env_format = null; } } $server->history_username_env_mode = $history_username_env_mode; From faf8548b0d51cd42e67ad7f6902e169e371be13a Mon Sep 17 00:00:00 2001 From: "Curious, aren't we?" <18015488+Msprg@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:08:11 +0100 Subject: [PATCH 3/9] Update scripts/sync.php Default get_global_history_username_env_enabled to false instead of true to avoid breaking exisitng deployments Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- scripts/sync.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sync.php b/scripts/sync.php index 32a66db..5613b36 100755 --- a/scripts/sync.php +++ b/scripts/sync.php @@ -510,7 +510,7 @@ function normalize_history_username_env_format($format) { function get_global_history_username_env_enabled() { global $config; if(!isset($config['privacy']) || !isset($config['privacy']['history_username_env_default'])) { - return true; + return false; } return intval($config['privacy']['history_username_env_default']) === 1; } From 0dbb19f17c9296f21d04e1718013c6aacd2fead4 Mon Sep 17 00:00:00 2001 From: Msprg Date: Mon, 9 Feb 2026 16:27:10 +0000 Subject: [PATCH 4/9] Enhance history username environment validation in sync.php and server.php - Added validation functions for history username environment format and value. - Updated normalization and user history environment option functions to utilize new validation. - Improved handling of history username environment format in server settings. This ensures that invalid formats are properly handled, enhancing overall robustness. --- scripts/sync.php | 41 +++++++++++++++++++++++++++++++++++++++-- views/server.php | 23 ++++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/scripts/sync.php b/scripts/sync.php index 5613b36..dd6718e 100755 --- a/scripts/sync.php +++ b/scripts/sync.php @@ -499,9 +499,36 @@ function get_default_history_username_env_format() { return 'BASH_HISTORY_USERNAME={uid}'; } +function history_username_env_format_is_valid($format) { + if($format === '') { + return false; + } + if(preg_match('/[\r\n,=\'"\\\\]/', $format)) { + return false; + } + if(strpos($format, '{uid}') === false) { + return false; + } + if(!preg_match('/^[A-Za-z0-9 ._@:+{}-]+$/', $format)) { + return false; + } + $without_uid = str_replace('{uid}', '', $format); + return strpos($without_uid, '{') === false && strpos($without_uid, '}') === false; +} + +function history_username_env_value_is_valid($value) { + if($value === '') { + return false; + } + if(preg_match('/[\r\n,=\'"\\\\{}]/', $value)) { + return false; + } + return preg_match('/^[A-Za-z0-9 ._@:+-]+$/', $value) === 1; +} + function normalize_history_username_env_format($format) { $format = trim((string)$format); - if($format === '' || strpos($format, '{uid}') === false) { + if(!history_username_env_format_is_valid($format)) { return get_default_history_username_env_format(); } return $format; @@ -560,6 +587,9 @@ function get_server_history_username_env_format($server) { } function escape_authorized_keys_option_value($value) { + if(!history_username_env_value_is_valid($value)) { + throw new InvalidArgumentException('Invalid history username environment value'); + } return str_replace(array('\\', '"'), array('\\\\', '\\"'), $value); } @@ -576,7 +606,14 @@ function get_user_history_username_env_option($user, $server) { return null; } $value = str_replace('{uid}', $user->uid, get_server_history_username_env_format($server)); - return 'environment="'.escape_authorized_keys_option_value($value).'"'; + if(!history_username_env_value_is_valid($value)) { + return null; + } + try { + return 'environment="'.escape_authorized_keys_option_value($value).'"'; + } catch(InvalidArgumentException $e) { + return null; + } } function add_user_history_username_env_option($prefix, $user, $server) { diff --git a/views/server.php b/views/server.php index 6b9d805..ec27cd2 100644 --- a/views/server.php +++ b/views/server.php @@ -38,6 +38,23 @@ $ldap_access_options = $server->list_ldap_access_options(); $server_admin_can_reset_host_key = (isset($config['security']) && isset($config['security']['host_key_reset_restriction']) && $config['security']['host_key_reset_restriction'] == 0); +function history_username_env_format_is_valid($format): bool { + if($format === '') { + return false; + } + if(preg_match('/[\r\n,=\'"\\\\]/', $format)) { + return false; + } + if(strpos($format, '{uid}') === false) { + return false; + } + if(!preg_match('/^[A-Za-z0-9 ._@:+{}-]+$/', $format)) { + return false; + } + $without_uid = str_replace('{uid}', '', $format); + return strpos($without_uid, '{') === false && strpos($without_uid, '}') === false; +} + if(isset($_POST['sync'])) { $server->sync_access(); redirect(); @@ -121,12 +138,16 @@ $server->key_management = $_POST['key_management']; $server->authorization = $_POST['authorization']; $server->key_scan = $_POST['key_scan']; + $history_username_env_mode = isset($_POST['history_username_env_mode']) ? $_POST['history_username_env_mode'] : 'inherit'; + if($history_username_env_mode !== 'inherit' && $history_username_env_mode !== 'enabled' && $history_username_env_mode !== 'disabled') { + $history_username_env_mode = 'inherit'; + } $history_username_env_format = null; if(isset($_POST['history_username_env_format'])) { $history_username_env_format = trim($_POST['history_username_env_format']); if($history_username_env_format === '') { $history_username_env_format = null; - } else if(!preg_match('/^[a-zA-Z0-9_\-={}]+$/', $history_username_env_format)) { + } elseif(!history_username_env_format_is_valid($history_username_env_format)) { $alert = new UserAlert; $alert->content = "Invalid history username env format. Only alphanumeric characters, underscores, hyphens, equals signs, and curly braces are allowed."; $alert->class = 'danger'; From 9c8a8d5a64414cee5142a2c80463cff1f6feea47 Mon Sep 17 00:00:00 2001 From: Msprg Date: Mon, 9 Feb 2026 16:38:51 +0000 Subject: [PATCH 5/9] Enhance value sanitization in escape_authorized_keys_option_value function in sync.php - Added a regex to remove control characters from the input value. - Ensured that null values are converted to empty strings before validation. This improves the robustness of the authorized keys option handling. --- scripts/sync.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/sync.php b/scripts/sync.php index dd6718e..11f3c19 100755 --- a/scripts/sync.php +++ b/scripts/sync.php @@ -587,6 +587,10 @@ function get_server_history_username_env_format($server) { } function escape_authorized_keys_option_value($value) { + $value = preg_replace('/[[:cntrl:]]+/', '', (string)$value); + if($value === null) { + $value = ''; + } if(!history_username_env_value_is_valid($value)) { throw new InvalidArgumentException('Invalid history username environment value'); } From ced253aaf9a7a5846f7257666acf3b785a45600a Mon Sep 17 00:00:00 2001 From: Msprg Date: Mon, 9 Feb 2026 17:09:05 +0000 Subject: [PATCH 6/9] Refine regex patterns for history username environment validation in sync.php - Updated regex to exclude additional characters in format and value validation functions. - Enhanced the allowed character set for the format to include '=' and '-' for better flexibility. This improves the accuracy of validation for history username environment inputs. --- scripts/sync.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/sync.php b/scripts/sync.php index 11f3c19..8e7a39c 100755 --- a/scripts/sync.php +++ b/scripts/sync.php @@ -503,13 +503,13 @@ function history_username_env_format_is_valid($format) { if($format === '') { return false; } - if(preg_match('/[\r\n,=\'"\\\\]/', $format)) { + if(preg_match('/[\r\n,\'"\\\\]/', $format)) { return false; } if(strpos($format, '{uid}') === false) { return false; } - if(!preg_match('/^[A-Za-z0-9 ._@:+{}-]+$/', $format)) { + if(!preg_match('/^[A-Za-z0-9 ._@:+={}-]+$/', $format)) { return false; } $without_uid = str_replace('{uid}', '', $format); @@ -520,10 +520,10 @@ function history_username_env_value_is_valid($value) { if($value === '') { return false; } - if(preg_match('/[\r\n,=\'"\\\\{}]/', $value)) { + if(preg_match('/[\r\n,\'"\\\\{}]/', $value)) { return false; } - return preg_match('/^[A-Za-z0-9 ._@:+-]+$/', $value) === 1; + return preg_match('/^[A-Za-z0-9 ._@:+=-]+$/', $value) === 1; } function normalize_history_username_env_format($format) { From c815a4c87a04792ff94025f29f0f02d21cdd2e4e Mon Sep 17 00:00:00 2001 From: Msprg Date: Mon, 9 Feb 2026 18:04:18 +0000 Subject: [PATCH 7/9] Add history username environment validation function to common file - Moved the history username environment format validation function from sync.php and server.php to a new common file, history_username_env_common.php. - Updated sync.php and server.php to include the new common file for validation, improving code organization and reusability. This change centralizes the validation logic, enhancing maintainability and consistency across the application. --- history_username_env_common.php | 18 ++++++++++++++++++ scripts/sync.php | 18 +----------------- views/server.php | 18 +----------------- 3 files changed, 20 insertions(+), 34 deletions(-) create mode 100644 history_username_env_common.php diff --git a/history_username_env_common.php b/history_username_env_common.php new file mode 100644 index 0000000..2f11df7 --- /dev/null +++ b/history_username_env_common.php @@ -0,0 +1,18 @@ +list_accounts(); $ldap_access_options = $server->list_ldap_access_options(); $server_admin_can_reset_host_key = (isset($config['security']) && isset($config['security']['host_key_reset_restriction']) && $config['security']['host_key_reset_restriction'] == 0); - -function history_username_env_format_is_valid($format): bool { - if($format === '') { - return false; - } - if(preg_match('/[\r\n,=\'"\\\\]/', $format)) { - return false; - } - if(strpos($format, '{uid}') === false) { - return false; - } - if(!preg_match('/^[A-Za-z0-9 ._@:+{}-]+$/', $format)) { - return false; - } - $without_uid = str_replace('{uid}', '', $format); - return strpos($without_uid, '{') === false && strpos($without_uid, '}') === false; -} +require_once('history_username_env_common.php'); if(isset($_POST['sync'])) { $server->sync_access(); From 5f3a3389bbb3c8282ba2ff397aff7981f184bd75 Mon Sep 17 00:00:00 2001 From: Msprg Date: Mon, 9 Feb 2026 18:36:30 +0000 Subject: [PATCH 8/9] Update error message for history username environment format validation in server.php - Revised the alert content to specify the allowed characters and format requirements for the history username environment. - This change enhances clarity for users encountering validation errors, ensuring they understand the expected input format. --- views/server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/server.php b/views/server.php index 8800f66..1d04377 100644 --- a/views/server.php +++ b/views/server.php @@ -133,7 +133,7 @@ $history_username_env_format = null; } elseif(!history_username_env_format_is_valid($history_username_env_format)) { $alert = new UserAlert; - $alert->content = "Invalid history username env format. Only alphanumeric characters, underscores, hyphens, equals signs, and curly braces are allowed."; + $alert->content = "Invalid history username env format. Allowed characters: letters, digits, spaces, dot (.), underscore (_), hyphen (-), at sign (@), colon (:), plus (+), equals (=), and braces for {uid}. The format must include {uid}."; $alert->class = 'danger'; $active_user->add_alert($alert); $history_username_env_format = null; From 02cc024bda42cda93251ac5863c0b66cfc3c18d6 Mon Sep 17 00:00:00 2001 From: Msprg Date: Mon, 9 Feb 2026 18:39:41 +0000 Subject: [PATCH 9/9] Enhance validation for history username environment format in common file - Added a check to ensure the format includes an '=' character in the history_username_env_format_is_valid function. - Updated the error message in server.php to reflect the new validation requirement, clarifying that both '=' and '{uid}' must be present. This improves the accuracy of format validation and user feedback for history username environments. --- history_username_env_common.php | 3 +++ views/server.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/history_username_env_common.php b/history_username_env_common.php index 2f11df7..338daa8 100644 --- a/history_username_env_common.php +++ b/history_username_env_common.php @@ -10,6 +10,9 @@ function history_username_env_format_is_valid($format) { if(!preg_match('/^[A-Za-z0-9 ._@:+={}-]+$/', $format)) { return false; } + if(strpos($format, '=') === false) { + return false; + } if(strpos($format, '{uid}') === false) { return false; } diff --git a/views/server.php b/views/server.php index 1d04377..bfa0acd 100644 --- a/views/server.php +++ b/views/server.php @@ -133,7 +133,7 @@ $history_username_env_format = null; } elseif(!history_username_env_format_is_valid($history_username_env_format)) { $alert = new UserAlert; - $alert->content = "Invalid history username env format. Allowed characters: letters, digits, spaces, dot (.), underscore (_), hyphen (-), at sign (@), colon (:), plus (+), equals (=), and braces for {uid}. The format must include {uid}."; + $alert->content = "Invalid history username env format. Allowed characters: letters, digits, spaces, dot (.), underscore (_), hyphen (-), at sign (@), colon (:), plus (+), equals (=), and braces for {uid}. The format must include both '=' and {uid}."; $alert->class = 'danger'; $active_user->add_alert($alert); $history_username_env_format = null;