Secure Coding: Best Practices zum Einsatz von Java NIO gegen Path Traversal
Das NIO-Paket von Java bietet robuste Tools zum Ändern der Datei- und Pfadnamen, mit denen Developer die Risiken durch Path Traversal (CWE-22) mindern können.
- Sven Ruppert
Welche Gefahren durch die kritische Schwachstelle CWE-22 (Path Traversal) in der Softwareentwicklung auftreten und welche Maßnahmen Developer dagegen unternehmen können, hat bereits der Artikel Secure Coding: Unbefugten Zugriff durch Path Traversal (CWE-22) verhindern beschrieben. Im Folgenden geht es nun konkret darum, wie sich die Werkzeuge aus dem Paket New I/O (NIO) von Java gegen die mit CWE-22 verbundenen Risiken einsetzen lassen. Anhand von Best Practices wird Schritt für Schritt gezeigt, wie sich die robusten NIO-Tools für die Datei- und Pfadverwaltung gezielt nutzen lassen, um Schwachstellen beim Path Traversal zu verhindern.
Pfade normalisieren
Durch die Normalisierung werden alle redundanten Elemente aus einem Pfad entfernt, beispielsweise "." (aktuelles Verzeichnis) und ".." (übergeordnetes Verzeichnis). Dieser Prozess trägt dazu bei sicherzustellen, dass Pfade angemessen strukturiert sind, und verhindert Path-Traversal-Angriffe.
Erstellen Sie den Basispfad: Definieren Sie das Basisverzeichnis, anhand dessen die Benutzereingaben aufgelöst werden. Dies sollte das Verzeichnis sein, auf das Sie den Zugriff beschränken möchten.
Path basePath = Paths.get("/var/www/uploads").normalize();
Lösen Sie die Benutzereingabe auf: Kombinieren Sie den Basispfad mit der vom Benutzer bereitgestellten Eingabe, um einen vollständigen Pfad zu erstellen. Dieser Schritt ist von entscheidender Bedeutung, um sicherzustellen, dass alle vom Benutzer angegebenen relativen Pfade im Kontext des Basispfades interpretiert werden.
String userInput = request.getParameter("file");
Path resolvedPath = basePath.resolve(userInput);
Normalisieren Sie den aufgelösten Pfad, um alle redundanten Elemente zu entfernen. Dieser Schritt stellt sicher, dass alle "." oder ".." im Pfad korrekt aufgelöst werden.
Path normalizedPath = resolvedPath.normalize();
Validieren Sie den normalisierten Pfad: Stellen Sie sicher, dass der normalisierte Pfad mit dem Basispfad beginnt. Durch diese Prüfung wird sichergestellt, dass der Pfad nicht außerhalb des vorgesehenen Verzeichnisses verläuft.
if (!normalizedPath.startsWith(basePath)) {
throw new SecurityException("Invalid file path: path traversal attempt detected.");
}
Überprüfen Sie den Pfad, um sicherzustellen, dass er innerhalb eines Basisverzeichnisses bleibt: Stellen Sie sicher, dass das Basisverzeichnis auf seine einfachste Form normalisiert ist. Kombinieren Sie das Basisverzeichnis mit dem vom Benutzer bereitgestellten Pfad und normalisieren Sie dann den resultierenden Pfad. Legen Sie fest, dass der endgültige normalisierte Pfad mit dem Basisverzeichnis beginnt, um sicherzustellen, dass er nicht außerhalb des vorgesehenen Verzeichnisses verläuft.
Verwenden Sie sichere Verzeichnis- und Dateiberechtigungen
Das Verwenden sicherer Verzeichnis- und Dateiberechtigungen ist wichtig, um die Sicherheit und Integrität Ihrer Dateien und Verzeichnisse zu gewährleisten, insbesondere beim Umgang mit Benutzereingaben in Java-Anwendungen.
Java NIO stellt APIs zum Festlegen und ĂśberprĂĽfen von Dateiberechtigungen bereit. Die Klassen PosixFilePermissions
und PosixFileAttributeView
können dies tun.
Festlegen von Berechtigungen fĂĽr ein Verzeichnis
Um Berechtigungen für ein Verzeichnis festzulegen, können Sie Files.createDirectories
und PosixFilePermissions
verwenden:
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
import java.io.IOException;
public class SecureFileHandler {
/**
* Creates a directory with secure permissions.
*
* @param dirPath The path of the directory to create.
* @throws IOException if an I/O error occurs.
*/
public static void createSecureDirectory(Path dirPath) throws IOException {
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---");
FileAttribute<Set<PosixFilePermission>> attr
= PosixFilePermissions.asFileAttribute(perms);
Files.createDirectories(dirPath, attr);
}
/**
* Example usage of creating a secure directory.
*
* @param args Command line arguments.
*/
public static void main(String[] args) {
Path dirPath = Paths.get("/var/www/uploads");
try {
createSecureDirectory(dirPath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Festlegen von Berechtigungen fĂĽr Dateien
Ebenso können Sie Berechtigungen für Dateien mit der Methode Files.setPosixFilePermissions
festlegen:
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.io.IOException;
import java.util.Set;
public class SecureFileHandler {
/**
* Sets secure permissions for a file.
*
* @param filePath The path of the file.
* @throws IOException if an I/O error occurs.
*/
public static void setSecureFilePermissions(Path filePath) throws IOException {
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-r-----");
Files.setPosixFilePermissions(filePath, perms);
}
/**
* Example usage of setting secure file permissions.
*
* @param args Command line arguments.
*/
public static void main(String[] args) {
Path filePath = Paths.get("/var/www/uploads/example.txt");
try {
setSecureFilePermissions(filePath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Dateiberechtigungen prĂĽfen
Bevor Sie Dateivorgänge ausführen, ist es wichtig zu prüfen, ob die Datei oder das Verzeichnis über die korrekten Berechtigungen verfügen. So können Sie das machen:
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.PosixFilePermission;
import java.io.IOException;
import java.util.Set;
public class SecureFileHandler {
/**
* Checks if a file has the required permissions.
*
* @param filePath The path of the file.
* @param requiredPerms The required permissions.
* @return True if the file has the required permissions, false otherwise.
* @throws IOException if an I/O error occurs.
*/
public static boolean hasRequiredPermissions(Path filePath,
Set<PosixFilePermission> requiredPerms)
throws IOException {
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(filePath);
return perms.containsAll(requiredPerms);
}
/**
* Example usage of checking file permissions.
*
* @param args Command line arguments.
*/
public static void main(String[] args) {
Path filePath = Paths.get("/var/www/uploads/example.txt");
Set<PosixFilePermission> requiredPerms = PosixFilePermissions.fromString("rw-r-----");
try {
if (hasRequiredPermissions(filePath, requiredPerms)) {
System.out.println("File has the required permissions.");
} else {
System.out.println("File does not have the required permissions.");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Praktische SicherheitsĂĽberlegungen
Schreibzugriff einschränken: Stellen Sie sicher, dass der Schreibzugriff auf die erforderlichen Benutzer und Prozesse beschränkt ist. Dadurch wird das Risiko unbefugter Änderungen minimiert.
Lesezugriff: Legen Sie schreibgeschützte Berechtigungen für Dateien fest, die nicht geändert werden müssen, um unbefugte Änderungen zu verhindern.
Ausführungsberechtigungen: Seien Sie vorsichtig, wenn Sie Ausführungsberechtigungen erteilen. Gewähren Sie nur den erforderlichen Benutzern Ausführungsberechtigungen und stellen Sie sicher, dass Skripte oder ausführbare Dateien sicher sind.
Besitzer- und Gruppenberechtigungen: Legen Sie die entsprechenden Besitzer- und Gruppenberechtigungen fest. Stellen Sie sicher, dass vertrauliche Dateien und Verzeichnisse dem richtigen Benutzer und der richtigen Gruppe gehören.
Symbolische Links: Vermeiden Sie es nach Möglichkeit, symbolischen Links zu folgen. Nur so lässt sich verhindern, dass Angreifer Sicherheitskontrollen mithilfe symbolischer Linkangriffe umgehen.
Verwendung von umask: Konfigurieren Sie den Wert umask
, um die Standardberechtigungseinstellungen für neu erstellte Dateien und Verzeichnisse zu steuern. Dies gewährleistet eine Grundsicherheit.
Verwendung der Klassen Path
und Files
von Java NIO und die richtigen Berechtigungseinstellungen kann die Sicherheit Ihrer Datei- und Verzeichnisvorgänge erheblich verbessern. Diese Vorgehensweisen tragen dazu bei, das Risiko unbefugter Zugriffe und Änderungen zu verringern und Ihre Anwendungen vor potenziellen Schwachstellen im Zusammenhang mit CWE-22 (Path Traversal) zu schützen.
Behandeln Sie Symlinks sicher
Der sichere Umgang mit symbolischen Links (Symlinks) ist von entscheidender Bedeutung, um potenzielle Sicherheitsrisiken wie das Umgehen von Zugriffskontrollen und Path-Traversal-Angriffe zu verhindern. Angreifer können Symlinks ausnutzen, um sich unbefugten Zugriff auf Dateien und Verzeichnisse zu verschaffen. Die folgenden Best Practices helfen Ihnen, den sicheren Umgang mit Symlinks in Java mithilfe der NIO-API (New I/O) zu gewährleisten:
Vermeiden Sie es, Symlinks zu folgen: Verwenden Sie die Option NOFOLLOW_LINKS
, wenn Sie Dateivorgänge ausführen, um das Folgen von Symlinks zu vermeiden. Dadurch wird sichergestellt, dass Vorgänge für den Symlink und nicht für die Zieldatei oder das Zielverzeichnis ausgeführt werden.
Validieren Sie das Ziel von Symlinks: Wenn Ihre Anwendung Symlinks folgen muss, validieren Sie das Ziel des Symlinks, um sicherzustellen, dass es auf einen zulässigen Speicherort verweist.
Suchen Sie nach Symlinks: ĂśberprĂĽfen Sie explizit, ob ein Pfad ein Symlink ist, und behandeln Sie ihn entsprechend.
Beispiel 1: So können Sie das Befolgen von Symlinks bei Dateivorgängen mit Java NIO vermeiden:
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
public class SecureSymlinkHandler {
/**
* Checks if the given path is a symbolic link.
*
* @param path The path to check.
* @return True if the path is a symbolic link, false otherwise.
* @throws IOException if an I/O error occurs.
*/
public static boolean isSymlink(Path path) throws IOException {
return Files.isSymbolicLink(path);
}
/**
* Safely deletes a file without following symbolic links.
*
* @param path The path to the file to delete.
* @throws IOException if an I/O error occurs.
*/
public static void safeDelete(Path path) throws IOException {
if (isSymlink(path)) {
throw new SecurityException("Refusing to delete symbolic link: " + path);
}
Files.delete(path);
}
/**
* Safely reads a file's attributes without following symbolic links.
*
* @param path The path to the file.
* @return The file's attributes.
* @throws IOException if an I/O error occurs.
*/
public static BasicFileAttributes safeReadAttributes(Path path) throws IOException {
return Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
}
public static void main(String[] args) {
Path path = Paths.get("/var/www/uploads/example.txt");
try {
if (isSymlink(path)) {
System.out.println("Path is a symbolic link.");
} else {
System.out.println("Path is not a symbolic link.");
BasicFileAttributes attrs = safeReadAttributes(path);
System.out.println("File size: " + attrs.size());
safeDelete(path);
System.out.println("File deleted safely.");
}
} catch (IOException | SecurityException e) {
e.printStackTrace();
}
}
}
Beispiel 2: Validieren Sie das Ziel von Symlinks. Wenn Ihre Anwendung Symlinks folgen muss, validieren Sie deren Ziele, um sicherzustellen, dass sie auf einen akzeptablen (sicheren) Speicherort verweisen:
import java.nio.file.*;
import java.io.IOException;
public class SecureSymlinkHandler {
/**
* Validates that the symlink's target is within the allowed base directory.
*
* @param symlink The symbolic link to validate.
* @param baseDir The allowed base directory.
* @throws IOException if an I/O error occurs or if validation fails.
*/
public static void validateSymlinkTarget(Path symlink, Path baseDir) throws IOException {
if (!Files.isSymbolicLink(symlink)) {
throw new IllegalArgumentException("Path is not a symbolic link: " + symlink);
}
Path target = Files.readSymbolicLink(symlink).normalize();
Path resolvedTarget = baseDir.resolve(target).normalize();
if (!resolvedTarget.startsWith(baseDir)) {
throw new SecurityException("Invalid symlink target: " + resolvedTarget);
}
}
public static void main(String[] args) {
Path symlink = Paths.get("/var/www/uploads/symlink");
Path baseDir = Paths.get("/var/www/uploads").normalize();
try {
validateSymlinkTarget(symlink, baseDir);
System.out.println("Symlink target is valid and within the allowed base directory.");
} catch (IOException | SecurityException e) {
e.printStackTrace();
}
}
}
Erlauben Sie nur vertrauenswürdigen Benutzern, Symlinks zu erstellen. Dies minimiert das Risiko, dass Symlinks für böswillige Zwecke verwendet werden. Überprüfen Sie regelmäßig symbolische Links in Ihrer Anwendung, um sicherzustellen, dass sie nicht auf nicht autorisierte Speicherorte verweisen. Verwenden Sie Bibliotheken, die Symlinks kennen und sicher verarbeiten. Dies kann dazu beitragen, das Risiko eines unbeabsichtigten Symlink-Folgens zu verringern. Stellen Sie sicher, dass Ihre Anwendung mit den minimal erforderlichen Berechtigungen ausgeführt wird, um die Auswirkungen potenzieller Schwachstellen im Zusammenhang mit Symlinks zu minimieren. Nutzen Sie mehrere Ebenen von Sicherheitskontrollen, um sich vor Symlink-Angriffen zu schützen. Dazu gehören Dateisystemberechtigungen, Prüfungen auf Anwendungsebene und regelmäßige Überwachung.
Validieren Sie Benutzereingaben grĂĽndlich
Das strenge Validieren von Benutzereingaben ist von entscheidender Bedeutung, um die Sicherheit und Integrität einer Anwendung zu gewährleisten. Eine ordnungsgemäße Eingabevalidierung hilft, verschiedene Angriffe zu verhindern, darunter Path Traversal (CWE-22), SQL-Injection, Cross-Site-Scripting (XSS) und mehr. Die folgenden Best Practices und Techniken helfen Ihnen dabei, eine strenge Validierung von Benutzereingaben in Java-Anwendungen umzusetzen:
- Whitelist-Validierung: Nur Eingaben zulassen, die einem vordefinierten Satz akzeptabler Werte entsprechen. Dies ist die sicherste Form der Validierung.
- Blacklist-Validierung: Eingaben ablehnen, die bekanntermaßen gefährliche Zeichen oder Muster enthalten. Dieser Ansatz ist weniger sicher als Whitelisting, lässt sich aber als zusätzliche Maßnahme einsetzen.
- Längenprüfungen: Stellen Sie sicher, dass die Eingaben innerhalb der erwarteten Längengrenzen liegen. Dies verhindert Pufferüberläufe und Denial-of-Service-Angriffe (DoS).
- DatentypprĂĽfungen: Stellen Sie sicher, dass die Eingaben mit dem erwarteten Datentyp ĂĽbereinstimmen (z. B. Ganzzahlen, Datumsangaben).
- Kodierung und Escape: Eingaben kodieren und maskieren, um Injektionsangriffe in verschiedenen Kontexten (z. B. HTML, SQL) zu verhindern.
- Kanonisierung: Eingaben vor der Validierung in ein Standardformat konvertieren. Das hilft, Eingaben sicher zu vergleichen und zu verarbeiten.
- Erlaubte Zeichen einschränken: Validieren Sie Dateinamen und Pfade anhand einer Whitelist zulässiger Zeichen. Lehnen Sie alle Eingaben ab, die Zeichen enthalten, die die Pfadstruktur ändern können (z. B. .., /, \).
- Überprüfen Sie den Pfad anhand des Basisverzeichnisses: Stellen Sie sicher, dass der aufgelöste und normalisierte Pfad mit dem vorgesehenen Basisverzeichnis beginnt.
Nachfolgend finden Sie eine Beispielimplementierung in Java, die diese Prinzipien und Techniken zur Validierung von Benutzereingaben, insbesondere fĂĽr Dateipfade, kombiniert:
import java.nio.file.*;
import java.util.Set;
import java.util.HashSet;
import java.util.logging.Logger;
import java.io.IOException;
import java.util.regex.Pattern;
public class SecureInputValidator {
private static final Logger logger = Logger.getLogger(SecureInputValidator.class.getName());
private static final Set<String> ALLOWED_EXTENSIONS = new HashSet<>();
static {
ALLOWED_EXTENSIONS.add(".txt");
ALLOWED_EXTENSIONS.add(".jpg");
ALLOWED_EXTENSIONS.add(".png");
ALLOWED_EXTENSIONS.add(".pdf");
}
/**
* Validates the user-provided file name against a whitelist of allowed characters and extensions.
*
* @param fileName The user-provided file name.
* @throws IllegalArgumentException if the file name is invalid.
*/
public static void validateFileName(String fileName) throws IllegalArgumentException {
if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File name cannot be null or empty.");
}
// Check for invalid characters
Pattern pattern = Pattern.compile("[^a-zA-Z0-9._-]");
if (pattern.matcher(fileName).find()) {
throw new IllegalArgumentException("File name contains invalid characters.");
}
// Check for allowed file extensions
boolean validExtension = ALLOWED_EXTENSIONS.stream().anyMatch(fileName::endsWith);
if (!validExtension) {
throw new IllegalArgumentException("File extension is not allowed.");
}
}
/**
* Validates the user-provided path to ensure it stays within the base directory.
*
* @param baseDir The base directory.
* @param userInput The user-provided input.
* @return The validated and normalized path.
* @throws SecurityException if a path traversal attempt is detected.
* @throws IllegalArgumentException if the file name is invalid.
* @throws IOException if an I/O error occurs.
*/
public static Path getSecureFilePath(String baseDir, String userInput) throws SecurityException, IllegalArgumentException, IOException {
validateFileName(userInput);
// Normalize the base directory
Path basePath = Paths.get(baseDir).normalize();
// Resolve the user input against the base directory and normalize the result
Path resolvedPath = basePath.resolve(userInput).normalize();
// Validate that the resolved path starts with the base directory
if (!resolvedPath.startsWith(basePath)) {
logSuspiciousActivity(userInput);
throw new SecurityException("Invalid file path: path traversal attempt detected.");
}
return resolvedPath;
}
/**
* Logs suspicious activity for further analysis.
*
* @param userInput The suspicious user input.
*/
private static void logSuspiciousActivity(String userInput) {
logger.warning("Suspicious file access attempt: " + userInput);
}
/**
* Example usage of the secure file path validation.
*
* @param args Command line arguments.
*/
public static void main(String[] args) {
String baseDir = "/var/www/uploads";
String userInput = "example.txt";
try {
Path filePath = getSecureFilePath(baseDir, userInput);
System.out.println("Validated file path: " + filePath);
} catch (SecurityException | IllegalArgumentException | IOException e) {
e.printStackTrace();
}
}
}