CVE-2023-25802: Exposure of Resource to Wrong Sphere (CWE-668)
value: Roxy-WI is a Web interface for managing Haproxy, Nginx, Apache, and Keepalived servers. Versions prior to 6.3.6.0 don't correctly neutralize `dir/../filename` sequences, such as `/etc/nginx/../passwd`, allowing an actor to gain information about a server. Version 6.3.6.0 has a patch for this issue.
CWE description: The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.
CVSS v3 base score: 7.5
Published: 2023-03-13T20:15Z
Repository: https://github.com/hap-wi/roxy-wi
Language: HTML
Files changed: app/modules/roxywi/logs.py, app/modules/server/ssh.py, app/scripts/ansible/roles/haproxy/tasks/logs.yml, app/templates/ajax/new_ssh.html, app/templates/base.html, app/templates/hapservers.html, app/templates/include/admin_add_user.html, app/templates/include/admin_ssh.html, app/templates/languages/en.html, app/templates/languages/ru.html, inc/overview.js
Commit date: 2023-02-22 10:36:20 +0300
Commit message: v6.3.6.0

Changelog: https://roxy-wi.org/changelog#6_3_6

Diff: diff --git a/app/modules/roxywi/logs.py b/app/modules/roxywi/logs.py
index ccef16ca..c89c007c 100644
--- a/app/modules/roxywi/logs.py
+++ b/app/modules/roxywi/logs.py
@@ -76,10 +76,12 @@ def show_roxy_log(
 	else:
 		exgrep_act = ''
 
 	log_file = checkAjaxInput(log_file) if log_file is not None else log_file
 
+	if '..' in log_file: return 'error: nice try'
+
 	if service in ('nginx', 'haproxy', 'apache', 'keepalived'):
 		syslog_server_enable = sql.get_setting('syslog_server_enable')
 		if syslog_server_enable is None or syslog_server_enable == 0:
 			if service == 'nginx':
 				local_path_logs = sql.get_setting('nginx_path_logs')
diff --git a/app/modules/server/ssh.py b/app/modules/server/ssh.py
index 5778c866..4df70bea 100644
--- a/app/modules/server/ssh.py
+++ b/app/modules/server/ssh.py
@@ -49,20 +49,20 @@ def ssh_connect(server_ip):
 
 
 def create_ssh_cred() -> None:
 	from jinja2 import Environment, FileSystemLoader
 
-	user_group = roxywi_common.get_user_group()
 	name = common.checkAjaxInput(form.getvalue('new_ssh'))
-	name = f'{name}_{user_group}'
 	enable = common.checkAjaxInput(form.getvalue('ssh_enable'))
 	group = common.checkAjaxInput(form.getvalue('new_group'))
+	group_name = sql.get_group_name_by_id(group)
 	username = common.checkAjaxInput(form.getvalue('ssh_user'))
 	password = common.checkAjaxInput(form.getvalue('ssh_pass'))
 	page = common.checkAjaxInput(form.getvalue('page'))
 	page = page.split("#")[0]
 	lang = roxywi_common.get_user_lang()
+	name = f'{name}_{group_name}'
 
 	if username is None or name is None:
 		print(error_mess)
 	else:
 		if sql.insert_new_ssh(name, enable, group, username, password):
@@ -89,10 +89,14 @@ def create_ssh_cread_api(name: str, enable: str, group: str, username: str, pass
 		if sql.insert_new_ssh(name, enable, group, username, password):
 			return True
 
 
 def upload_ssh_key(name: str, user_group: str, key: str) -> bool:
+	if '..' in name:
+		print('error: nice try')
+		return False
+
 	try:
 		key = paramiko.pkey.load_private_key(key)
 	except Exception as e:
 		print(f'error: Cannot save SSH key file: {e}')
 		return False
diff --git a/app/scripts/ansible/roles/haproxy/tasks/logs.yml b/app/scripts/ansible/roles/haproxy/tasks/logs.yml
index 96291c27..635fdc1a 100644
--- a/app/scripts/ansible/roles/haproxy/tasks/logs.yml
+++ b/app/scripts/ansible/roles/haproxy/tasks/logs.yml
@@ -9,6 +9,7 @@
 - name: Installing HAProxy conf for logrotate
   template:
     src: /var/www/haproxy-wi/app/scripts/ansible/roles/haproxy/templates/logrotate.conf.j2
     dest: /etc/logrotate.d/haproxy.conf
     force: no
+  when: (ansible_facts['os_family'] == "RedHat" or ansible_facts['os_family'] == 'CentOS')
   ignore_errors: yes
\ No newline at end of file
diff --git a/app/templates/ajax/new_ssh.html b/app/templates/ajax/new_ssh.html
index 86be6c63..d3a0bf51 100644
--- a/app/templates/ajax/new_ssh.html
+++ b/app/templates/ajax/new_ssh.html
@@ -13,11 +13,11 @@
 		</td>
 		{% if page != "servers.py" %}
 			<td>
 				<select id="sshgroup-{{ssh.id}}" name="sshgroup-{{ssh.id}}">
 					{% for group in groups %}
-						{% if ssh.5 == group.group_id %}
+						{% if ssh.groups == group.group_id %}
 							<option value="{{ group.group_id }}" selected>{{ group.name }}</option>
 						{% else %}
 							<option value="{{ group.group_id }}">{{ group.name }}</option>
 						{% endif %}
 					{% endfor %}
diff --git a/app/templates/base.html b/app/templates/base.html
index b78bcf75..c3838352 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -4,19 +4,21 @@
 		<title>{% block title %}{% endblock %}</title>
 		<meta charset="UTF-8">
 		<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
 		<meta http-equiv="Pragma" content="no-cache" />
 		<meta http-equiv="Expires" content="0" />
-		<meta id="translate" style="display: none;" data-delete="{{lang.words.delete|title()}}" data-cancel="{{lang.words.cancel|title()}}"  data-add="{{lang.words.add|title()}}" \
+		<meta id="translate" style="display: none;" data-delete="{{lang.words.delete|title()}}" data-cancel="{{lang.words.cancel|title()}}" data-add="{{lang.words.add|title()}}"
 			  data-superAdmin_pass="{{lang.phrases.superAdmin_pass}}" data-superAdmin_services="{{lang.phrases.superAdmin_services}}" data-change="{{lang.words.change|title()}}"
 			  data-change2="{{lang.words.change2|title()}}" data-password="{{lang.words.password}}" data-groups="{{lang.words.groups}}" data-save="{{lang.words.save|title()}}"
 			  data-manage="{{lang.words.manage|title()}}" data-services3="{{lang.words.services3}}" data-logout="{{lang.words.logout|title()}}"
 			  data-something_wrong="{{lang.words.something_wrong}}" data-check_logs="{{lang.words.check_logs}}" data-wait_mess="{{lang.phrases.wait_mess}}"
 			  data-just_save="{{lang.words.just|title()}} {{lang.words.save}}" data-upload_and_restart="{{lang.phrases.upload_and_restart|title()}}"
 			  data-are_you_sure="{{lang.phrases.are_you_sure}}" data-upload_and_reload="{{lang.phrases.upload_and_reload|title()}}" data-select_server="{{lang.errors.select_server}}"
 			  data-empty_name="{{lang.errors.empty_name}}" data-edit="{{lang.words.edit|title()}}" data-close="{{lang.words.close|title()}}" data-server_info="{{lang.phrases.server_info}}"
-			  data-generated_config="{{lang.words.generated|title()}} {{lang.words.config}}" />
+			  data-generated_config="{{lang.words.generated|title()}} {{lang.words.config}}" data-restart="{{lang.words.restart|title()}}" data-start="{{lang.words.start|title()}}"
+			  data-stop="{{lang.words.stop|title()}}" data-reload="{{lang.words.reload|title()}}" data-user_groups="{{lang.phrases.user_groups}}" data-settings="{{lang.words.settings|title()}}"
+			  data-for="{{lang.words.for}}" />
 		{% if title == 'Login page' %}
 		<meta name="viewport" content="width=device-width, user-scalable=1">
 		{% endif %}
 		<link href="/inc/images/favicon/favicon.ico" rel="icon" type="image/png" />
 		<link rel="apple-touch-icon" sizes="57x57" href="/inc/images/favicon/inc/images/favicon/apple-icon-57x57.png">
diff --git a/app/templates/hapservers.html b/app/templates/hapservers.html
index f6fb2c99..1c30fc1b 100644
--- a/app/templates/hapservers.html
+++ b/app/templates/hapservers.html
@@ -417,11 +417,11 @@
 		</div>
 		{% endif %}
 	{% endfor %}
 {% endif %}
 <div id="dialog-confirm" style="display: none;">
-  <p><span class="ui-icon ui-icon-alert" style="float:left; margin:3px 12px 20px 0;"></span>Are you sure?</p>
+  <p><span class="ui-icon ui-icon-alert" style="float:left; margin:3px 12px 20px 0;"></span>{{lang.phrases.are_you_sure}}</p>
 </div>
 <div id="dialog-settings-service" style="display: none;"></div>
 <script>
 	{% if serv %}
 		overviewHapserverBackends(ip, hostnamea, '{{service}}');
diff --git a/app/templates/include/admin_add_user.html b/app/templates/include/admin_add_user.html
index 0530edc0..74ca4f3d 100644
--- a/app/templates/include/admin_add_user.html
+++ b/app/templates/include/admin_add_user.html
@@ -1,31 +1,31 @@
 {% from 'include/input_macros.html' import input, checkbox %}
 <tr>
 	<td class="padding20">
-		New user
+		{{lang.words.name|title()}}
 		<span class="need-field">*</span>
 	</td>
 	<td>
 		{{ input('new-username') }}
 			{% if ldap_enable == 1 %}
-				<a href="#" title="Search user in AD" id="search_ldap_user">Search user in AD</a>
+				<a href="#" title="{{lang.phrases.search_id_ad}}" id="search_ldap_user">{{lang.phrases.search_in_ad}}</a>
 			{% endif %}
 	</td>
 </tr>
 <tr>
 	<td class="padding20">
-		Password
+		{{lang.words.password|title()}}
 		<span class="need-field">*</span>
 	</td>
 	<td>{{ input('new-password', type='password') }}</td>
 </tr>
 <tr>
-	<td class="padding20">Active</td>
+	<td class="padding20">{{lang.words.active|title()}}</td>
 	<td>{{ checkbox('activeuser', checked='checked') }}</td>
 </tr>
 <tr>
 	<td class="padding20">
-		Email
+		{{lang.words.email|title()}}
 		<span class="need-field">*</span>
 	</td>
 	<td>{{ input('new-email', type='email') }}</td>
 </tr>
\ No newline at end of file
diff --git a/app/templates/include/admin_ssh.html b/app/templates/include/admin_ssh.html
index 865b2808..e805ef05 100644
--- a/app/templates/include/admin_ssh.html
+++ b/app/templates/include/admin_ssh.html
@@ -29,11 +29,11 @@
         </td>
         {% if page != "servers.py" %}
         <td>
             <select id="sshgroup-{{ssh.id}}" name="sshgroup-{{ssh.id}}">
                 {% for group in groups %}
-                {% if ssh.5 == group.group_id %}
+                {% if ssh.groups == group.group_id %}
                 <option value="{{ group.group_id }}" selected>{{ group.name }}</option>
                 {% else %}
                 <option value="{{ group.group_id }}">{{ group.name }}</option>
                 {% endif %}
                 {% endfor %}
diff --git a/app/templates/languages/en.html b/app/templates/languages/en.html
index c0fac72e..c05a9344 100644
--- a/app/templates/languages/en.html
+++ b/app/templates/languages/en.html
@@ -290,10 +290,11 @@
     "section_from_server": "section from server",
     "how_to_install": "how to install",
     "port_check": "port check",
     "possible_service_name": "Possible service name",
     "server_info": "Server info",
+    "user_groups": "User groups",
     }
 %}
 {% set roles = {
     "superAdmin": "Has the highest level of administrative permissions and controls the actions of all other users",
     "admin": "Has access everywhere except the Admin area",
@@ -539,10 +540,11 @@
     "yes": "yes",
     "interval": "interval",
     "desc": "description",
     "login": "login",
     "role": "role",
+    "roles": "roles",
     "subs": "subscription",
     "show_all": "show all",
     "plan": "plan",
     "pay_method": "pay method",
     "active": "active",
diff --git a/app/templates/languages/ru.html b/app/templates/languages/ru.html
index f2b8b9e6..4c87657e 100644
--- a/app/templates/languages/ru.html
+++ b/app/templates/languages/ru.html
@@ -292,10 +292,11 @@
     "section_from_server": "секцию с сервера",
     "how_to_install": "как установить",
     "port_check": "проверка порта",
     "possible_service_name": "Возможное название службы",
     "server_info": "Информация о сервере",
+    "user_groups": "Пользовательские группы",
     }
 %}
 {% set roles = {
     "superAdmin": "Самый высокий уровень административных разрешений и контролирует действия всех других пользователей",
     "admin": "Доступ везде, кроме админки",
@@ -543,10 +544,11 @@
     "yes": "да",
     "interval": "интервал",
     "desc": "описание",
     "login": "логин",
     "role": "роль",
+    "roles": "роли",
     "subs": "подписка",
     "show_all": "показать все",
     "plan": "план",
     "pay_method": "платежный метод",
     "active": "активный",
diff --git a/inc/overview.js b/inc/overview.js
index 7f956725..22ed4f2c 100644
--- a/inc/overview.js
+++ b/inc/overview.js
@@ -407,23 +407,26 @@ $( function() {
 
 		updateHapWIServer(id[1], service_name)
 	});
 });
 function confirmAjaxAction(action, service, id) {
+	var cancel_word = $('#translate').attr('data-cancel');
+	var action_word = $('#translate').attr('data-'+action);
 	$( "#dialog-confirm" ).dialog({
 		resizable: false,
 		height: "auto",
 		width: 400,
 		modal: true,
-		title: "Are you sure you want to "+ action + " " + id + "?",
-		buttons: {
-			"Sure": function() {
-				$( this ).dialog( "close" );
-				if(service == "haproxy") {
+		title: action_word + " " + id + "?",
+		buttons: [{
+			text: action_word,
+			click: function () {
+				$(this).dialog("close");
+				if (service == "haproxy") {
 					ajaxActionServers(action, id);
-					if(action == "restart" || action == "reload") {
-						if(localStorage.getItem('restart')) {
+					if (action == "restart" || action == "reload") {
+						if (localStorage.getItem('restart')) {
 							localStorage.removeItem('restart');
 							$("#apply").css('display', 'none');
 						}
 					}
 				} else if (service == "waf") {
@@ -435,15 +438,17 @@ function confirmAjaxAction(action, service, id) {
 				} else if (service == "apache") {
 					ajaxActionApacheServers(action, id)
 				} else if (service == "waf_nginx") {
 					ajaxActionWafNginxServers(action, id)
 				}
-			},
-			Cancel: function() {
+			}
+		}, {
+			text: cancel_word,
+			click: function() {
 				$( this ).dialog( "close" );
 			}
-		}
+		}]
 	});
 }
 function updateHapWIServer(id, service_name) {
 	var alert_en = 0;
 	var metrics = 0;
@@ -626,10 +631,14 @@ function showSubOverview() {
 			}
 		}
 	} );
 }
 function serverSettings(id, name) {
+	var cancel_word = $('#translate').attr('data-cancel');
+	var save_word = $('#translate').attr('data-save');
+	var settings_word = $('#translate').attr('data-settings');
+	var for_word = $('#translate').attr('data-for');
 	var service = $('#service').val();
 	$.ajax({
 		url: "options.py",
 		data: {
 			serverSettings: id,
@@ -647,20 +656,23 @@ function serverSettings(id, name) {
 				$("#dialog-settings-service").dialog({
 					resizable: false,
 					height: "auto",
 					width: 400,
 					modal: true,
-					title: "Settings for " + name,
-					buttons: {
-						"Save": function () {
+					title: settings_word + " "+for_word+" " + name,
+					buttons: [{
+						text: save_word,
+						click: function () {
 							$(this).dialog("close");
 							serverSettingsSave(id, name, service, $(this));
-						},
-						Cancel: function () {
+						}
+					}, {
+						text: cancel_word,
+						click: function () {
 							$(this).dialog("close");
 						}
-					}
+					}]
 				});
 			}
 		}
 	});
 }

Vulnerable code: 	user_group = roxywi_common.get_user_group()
	name = f'{name}_{user_group}'
						{% if ssh.5 == group.group_id %}
		<meta id="translate" style="display: none;" data-delete="{{lang.words.delete|title()}}" data-cancel="{{lang.words.cancel|title()}}"  data-add="{{lang.words.add|title()}}" \
			  data-generated_config="{{lang.words.generated|title()}} {{lang.words.config}}" />
  <p><span class="ui-icon ui-icon-alert" style="float:left; margin:3px 12px 20px 0;"></span>Are you sure?</p>
		New user
				<a href="#" title="Search user in AD" id="search_ldap_user">Search user in AD</a>
		Password
	<td class="padding20">Active</td>
		Email
                {% if ssh.5 == group.group_id %}
		title: "Are you sure you want to "+ action + " " + id + "?",
		buttons: {
			"Sure": function() {
				$( this ).dialog( "close" );
				if(service == "haproxy") {
					if(action == "restart" || action == "reload") {
						if(localStorage.getItem('restart')) {
			},
			Cancel: function() {
		}
					title: "Settings for " + name,
					buttons: {
						"Save": function () {
						},
						Cancel: function () {
					}
Fixed code: 	if '..' in log_file: return 'error: nice try'

	group_name = sql.get_group_name_by_id(group)
	name = f'{name}_{group_name}'
	if '..' in name:
		print('error: nice try')
		return False

  when: (ansible_facts['os_family'] == "RedHat" or ansible_facts['os_family'] == 'CentOS')
						{% if ssh.groups == group.group_id %}
		<meta id="translate" style="display: none;" data-delete="{{lang.words.delete|title()}}" data-cancel="{{lang.words.cancel|title()}}" data-add="{{lang.words.add|title()}}"
			  data-generated_config="{{lang.words.generated|title()}} {{lang.words.config}}" data-restart="{{lang.words.restart|title()}}" data-start="{{lang.words.start|title()}}"
			  data-stop="{{lang.words.stop|title()}}" data-reload="{{lang.words.reload|title()}}" data-user_groups="{{lang.phrases.user_groups}}" data-settings="{{lang.words.settings|title()}}"
			  data-for="{{lang.words.for}}" />
  <p><span class="ui-icon ui-icon-alert" style="float:left; margin:3px 12px 20px 0;"></span>{{lang.phrases.are_you_sure}}</p>
		{{lang.words.name|title()}}
				<a href="#" title="{{lang.phrases.search_id_ad}}" id="search_ldap_user">{{lang.phrases.search_in_ad}}</a>
		{{lang.words.password|title()}}
	<td class="padding20">{{lang.words.active|title()}}</td>
		{{lang.words.email|title()}}
                {% if ssh.groups == group.group_id %}
    "user_groups": "User groups",
    "roles": "roles",
    "user_groups": "Пользовательские группы",
    "roles": "роли",
	var cancel_word = $('#translate').attr('data-cancel');
	var action_word = $('#translate').attr('data-'+action);
		title: action_word + " " + id + "?",
		buttons: [{
			text: action_word,
			click: function () {
				$(this).dialog("close");
				if (service == "haproxy") {
					if (action == "restart" || action == "reload") {
						if (localStorage.getItem('restart')) {
			}
		}, {
			text: cancel_word,
			click: function() {
		}]
	var cancel_word = $('#translate').attr('data-cancel');
	var save_word = $('#translate').attr('data-save');
	var settings_word = $('#translate').attr('data-settings');
	var for_word = $('#translate').attr('data-for');
					title: settings_word + " "+for_word+" " + name,
					buttons: [{
						text: save_word,
						click: function () {
						}
					}, {
						text: cancel_word,
						click: function () {
					}]