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:
Naiel
2026-02-19 10:35:31 +01:00
committed by GitHub
5 changed files with 103 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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