Merge pull request #10 from Axia4/copilot/restrict-safe-filename-characters
Harden path validation and file handling against directory traversal attacks
This commit is contained in:
@@ -105,8 +105,25 @@ $uploadErrors = [];
|
||||
|
||||
function safe_filename($name)
|
||||
{
|
||||
// Normalize to base name to avoid directory traversal
|
||||
$name = basename($name);
|
||||
return preg_replace("/[^a-zA-Z0-9._-]/", "_", $name);
|
||||
// Replace disallowed characters with underscore
|
||||
$name = preg_replace("/[^a-zA-Z0-9._-]/", "_", $name);
|
||||
// Remove leading dots to avoid hidden/special files like ".htaccess"
|
||||
$name = ltrim($name, '.');
|
||||
// Ensure there is at most one dot in the filename to prevent extension confusion
|
||||
if (substr_count($name, '.') > 1) {
|
||||
$parts = explode('.', $name);
|
||||
$ext = array_pop($parts);
|
||||
$base = implode('_', $parts);
|
||||
// Ensure extension is not empty
|
||||
if ($ext === '') {
|
||||
$name = $base === '' ? 'file' : $base;
|
||||
} else {
|
||||
$name = ($base === '' ? 'file' : $base) . '.' . $ext;
|
||||
}
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
function handle_image_upload($fieldName, $targetBaseName, $baseDir, &$uploadErrors)
|
||||
|
||||
@@ -46,11 +46,20 @@ if ($real_base === false) {
|
||||
// Get list of alumnos if not specified
|
||||
$alumnos_base_path = "$base_path/$centro_id/Aularios/$aulario_id/Alumnos";
|
||||
$alumnos = [];
|
||||
if (is_dir($alumnos_base_path)) {
|
||||
$alumnos = glob($alumnos_base_path . "/*", GLOB_ONLYDIR);
|
||||
usort($alumnos, function($a, $b) {
|
||||
return strcasecmp(basename($a), basename($b));
|
||||
});
|
||||
|
||||
// Resolve and validate alumnos path to ensure it stays within the allowed base directory
|
||||
$alumnos_real_path = realpath($alumnos_base_path);
|
||||
if ($alumnos_real_path !== false) {
|
||||
// Ensure the resolved path is within the expected base path
|
||||
$real_base_with_sep = rtrim($real_base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||
$alumnos_real_with_sep = rtrim($alumnos_real_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||
|
||||
if (strpos($alumnos_real_with_sep, $real_base_with_sep) === 0 && is_dir($alumnos_real_path)) {
|
||||
$alumnos = glob($alumnos_real_path . "/*", GLOB_ONLYDIR);
|
||||
usort($alumnos, function($a, $b) {
|
||||
return strcasecmp(basename($a), basename($b));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If no alumno specified, show list
|
||||
|
||||
@@ -4,7 +4,25 @@ require_once "_incl/tools.security.php";
|
||||
ini_set("display_errors", "0");
|
||||
// Funciones auxiliares para el diario
|
||||
function getDiarioPath($alumno, $centro_id, $aulario_id) {
|
||||
$base_path = "/DATA/entreaulas/Centros/$centro_id/Aularios/$aulario_id/Alumnos/$alumno";
|
||||
// Validate path components to avoid directory traversal or illegal characters
|
||||
// Allow only alphanumeric, underscore and dash for alumno and aulario_id
|
||||
$idPattern = '/^[A-Za-z0-9_-]+$/';
|
||||
// Typically centro_id is numeric; restrict it accordingly
|
||||
$centroPattern = '/^[0-9]+$/';
|
||||
|
||||
if (!preg_match($idPattern, (string)$alumno) ||
|
||||
!preg_match($idPattern, (string)$aulario_id) ||
|
||||
!preg_match($centroPattern, (string)$centro_id)) {
|
||||
// Invalid identifiers, do not construct a filesystem path
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extra safety: strip any directory components if present
|
||||
$alumno_safe = basename($alumno);
|
||||
$centro_safe = basename($centro_id);
|
||||
$aulario_safe = basename($aulario_id);
|
||||
|
||||
$base_path = "/DATA/entreaulas/Centros/$centro_safe/Aularios/$aulario_safe/Alumnos/$alumno_safe";
|
||||
return $base_path . "/Diario/" . date("Y-m-d");
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ if (in_array("entreaulas:docente", $_SESSION["auth_data"]["permissions"] ?? [])
|
||||
$aulario_id = Sf($_GET["aulario"] ?? "");
|
||||
$centro_id = $_SESSION["auth_data"]["entreaulas"]["centro"] ?? "";
|
||||
|
||||
if ($aulario_id === "" || $centro_id === "") {
|
||||
// Sanitize and validate centro_id and aulario_id to prevent directory traversal
|
||||
$centro_id = safe_filename($centro_id);
|
||||
$aulario_id = safe_filename($aulario_id);
|
||||
if ($aulario_id === "" || $centro_id === "" || strpos($centro_id, '..') !== false || strpos($aulario_id, '..') !== false) {
|
||||
require_once "_incl/pre-body.php";
|
||||
?>
|
||||
<div class="card pad">
|
||||
@@ -33,8 +36,25 @@ if (!is_dir($proyectos_dir)) {
|
||||
// Helper functions
|
||||
function safe_filename($name)
|
||||
{
|
||||
// Normalize to base name to avoid directory traversal
|
||||
$name = basename($name);
|
||||
return preg_replace("/[^a-zA-Z0-9._-]/", "_", $name);
|
||||
// Replace disallowed characters with underscore
|
||||
$name = preg_replace("/[^a-zA-Z0-9._-]/", "_", $name);
|
||||
// Remove leading dots to avoid hidden/special files like ".htaccess"
|
||||
$name = ltrim($name, '.');
|
||||
// Ensure there is at most one dot in the filename to prevent extension confusion
|
||||
if (substr_count($name, '.') > 1) {
|
||||
$parts = explode('.', $name);
|
||||
$ext = array_pop($parts);
|
||||
$base = implode('_', $parts);
|
||||
// Ensure extension is not empty
|
||||
if ($ext === '') {
|
||||
$name = $base === '' ? 'file' : $base;
|
||||
} else {
|
||||
$name = ($base === '' ? 'file' : $base) . '.' . $ext;
|
||||
}
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
function sanitize_html($html)
|
||||
@@ -193,7 +213,10 @@ function format_bytes($bytes)
|
||||
return $bytes . "B";
|
||||
}
|
||||
|
||||
$app_max_upload_bytes = 500 * 1024 * 1024;
|
||||
if (!defined('APP_MAX_UPLOAD_BYTES')) {
|
||||
define('APP_MAX_UPLOAD_BYTES', 500 * 1024 * 1024);
|
||||
}
|
||||
$app_max_upload_bytes = APP_MAX_UPLOAD_BYTES;
|
||||
$upload_limit = parse_size_to_bytes(ini_get("upload_max_filesize"));
|
||||
$post_limit = parse_size_to_bytes(ini_get("post_max_size"));
|
||||
$max_upload_bytes = $app_max_upload_bytes;
|
||||
|
||||
@@ -7,7 +7,22 @@ switch ($_GET['form'] ?? '') {
|
||||
if (empty($username)) {
|
||||
die("Nombre de usuario no proporcionado.");
|
||||
}
|
||||
$userdata_old = json_decode(file_get_contents("/DATA/Usuarios/$username.json"), true) ?? [];
|
||||
// Validate username to prevent directory traversal
|
||||
$username = basename($username);
|
||||
if (preg_match('/[^a-zA-Z0-9._-]/', $username) || strpos($username, '..') !== false) {
|
||||
die("Nombre de usuario inválido.");
|
||||
}
|
||||
$user_file = "/DATA/Usuarios/$username.json";
|
||||
$userdata_old = [];
|
||||
if (is_readable($user_file)) {
|
||||
$file_contents = file_get_contents($user_file);
|
||||
if ($file_contents !== false) {
|
||||
$decoded = json_decode($file_contents, true);
|
||||
if (is_array($decoded)) {
|
||||
$userdata_old = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
$userdata_new = [
|
||||
'display_name' => $_POST['display_name'] ?? '',
|
||||
'email' => $_POST['email'] ?? '',
|
||||
@@ -105,7 +120,16 @@ switch ($_GET['action'] ?? '') {
|
||||
case 'edit':
|
||||
require_once "_incl/pre-body.php";
|
||||
$username = Sf($_GET['user'] ?? '');
|
||||
$userdata = json_decode(file_get_contents("/DATA/Usuarios/$username.json"), true);
|
||||
// Validate username to prevent directory traversal
|
||||
$username = basename($username);
|
||||
if (preg_match('/[^a-zA-Z0-9._-]/', $username) || strpos($username, '..') !== false) {
|
||||
die("Nombre de usuario inválido.");
|
||||
}
|
||||
$user_file = "/DATA/Usuarios/$username.json";
|
||||
if (!file_exists($user_file) || !is_readable($user_file)) {
|
||||
die("Usuario no encontrado o datos no disponibles.");
|
||||
}
|
||||
$userdata = json_decode(file_get_contents($user_file), true) ?? [];
|
||||
?>
|
||||
<form method="post" action="?form=save_edit">
|
||||
<div class="card pad">
|
||||
|
||||
Reference in New Issue
Block a user