EJB Bad Practices: Verwendung von Java I/O
Beschreibung
EJB Bad Practices: Verwendung von Java I/O ist eine Schwachstelle, bei der ein Enterprise JavaBean die EJB-Spezifikation verletzt, indem es das java.io-Paket verwendet, um auf Dateien und Verzeichnisse im lokalen Dateisystem zuzugreifen. Die EJB-Spezifikation schreibt vor, dass Beans "das java.io-Paket nicht verwenden dürfen, um auf Dateien und Verzeichnisse im Dateisystem zuzugreifen." Diese Anforderung existiert, weil EJB-Container Beans über mehrere Server verteilen können, wo Dateisysteme nicht geteilt werden, was dateibasierte Operationen unzuverlässig und nicht portabel macht. Die Spezifikation empfiehlt stattdessen die Verwendung von Ressourcenmanager-APIs wie JDBC.
Risiko
Die Verwendung von java.io in EJBs erzeugt erhebliche Deployment- und Zuverlässigkeitsrisiken. Dateioperationen schlagen fehl, wenn Beans auf verschiedenen Servern in einem Cluster deployt werden, wo Dateipfade nicht existieren oder auf unterschiedlichen Inhalt zeigen. Konfigurationsdateien, die auf einem Server zugänglich sind, können auf einem anderen fehlen. Anwendungen werden nicht portabel über Umgebungen mit unterschiedlichen Dateisystemlayouts. Sicherheitsgrenzen, die vom EJB-Container durchgesetzt werden, können durch direkten Dateizugriff umgangen werden. Zusätzlich können Dateioperationen mit dem Container-Ressourcenmanagement in Konflikt geraten und Ressourcenlecks oder Konflikte verursachen. In geclusterten Deployments führt mangelnde Dateisynchronisation zu inkonsistentem Anwendungszustand.
Lösung
Verwenden Sie java.io nicht für Dateisystemzugriff in EJBs. Nutzen Sie stattdessen Container-verwaltete Ressourcen und geeignete APIs: JDBC für strukturierte Datenspeicherung, JNDI für Konfigurations- und Ressourcensuche, JPA für Objektpersistenz, JMS für asynchronen Datenaustausch oder verteilte Caching-Lösungen für gemeinsamen Zustand. Für Konfiguration verwenden Sie Umgebungseinträge, Systemeigenschaften oder externe Konfigurationsdienste. Wenn Dateizugriff absolut erforderlich ist, delegieren Sie an einen separaten Dienst außerhalb des EJB-Containers, verwenden Sie ein gemeinsames Netzwerk-Dateisystem mit angemessener Synchronisation oder nutzen Sie Cloud-Storage-APIs, die konsistent über alle Knoten funktionieren.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Sonstige | Bereich: Sonstige Qualitätsverschlechterung - Anwendungen werden nicht portabel und können in geclusterten oder verteilten Deployment-Umgebungen fehlschlagen. |
| Verfügbarkeit | Bereich: Verfügbarkeit DoS: Absturz, Beenden oder Neustart - Dateioperationen können unerwartet fehlschlagen, wenn erwartete Dateien nicht auf allen Cluster-Knoten vorhanden sind. |
Beispielcode
Verwundbarer Code
// Verwundbar: Konfiguration vom Dateisystem lesen
import javax.ejb.Stateless;
import java.io.*;
import java.util.Properties;
@Stateless
public class VulnerableConfigBean implements ConfigService {
// Verwundbar: java.io zum Lesen der Konfigurationsdatei verwenden
public Properties loadConfiguration() {
Properties props = new Properties();
// Verwundbar: Dateipfad existiert möglicherweise nicht auf allen Servern
try (FileInputStream fis = new FileInputStream("/config/app.properties")) {
props.load(fis);
} catch (IOException e) {
// Kann auf anderem Server im Cluster fehlschlagen
throw new RuntimeException("Konfiguration nicht gefunden", e);
}
return props;
}
// Verwundbar: In lokales Dateisystem schreiben
public void saveConfiguration(Properties props) {
// Verwundbar: Änderungen betreffen nur diesen Server
try (FileOutputStream fos = new FileOutputStream("/config/app.properties")) {
props.store(fos, "Aktualisierte Konfiguration");
} catch (IOException e) {
throw new RuntimeException("Kann Konfiguration nicht speichern", e);
}
// Andere Server im Cluster sehen diese Änderung nicht!
}
}
// Verwundbar: Dateibasierte Datenspeicherung in EJB
@Stateless
public class VulnerableDataBean implements DataService {
private static final String DATA_DIR = "/data/records/";
// Verwundbar: Daten in Dateien speichern
public void saveRecord(String id, String content) {
File file = new File(DATA_DIR + id + ".dat");
// Verwundbar: Dateioperationen in EJB
try (FileWriter writer = new FileWriter(file)) {
writer.write(content);
} catch (IOException e) {
throw new RuntimeException("Kann Datensatz nicht speichern", e);
}
}
// Verwundbar: Daten aus Dateien lesen
public String getRecord(String id) {
File file = new File(DATA_DIR + id + ".dat");
// Datei existiert möglicherweise nicht auf diesem Server
if (!file.exists()) {
return null; // Inkonsistent über Cluster
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line);
}
return content.toString();
} catch (IOException e) {
throw new RuntimeException("Kann Datensatz nicht lesen", e);
}
}
}
// Verwundbar: Dateibasiertes Logging in EJB
@Stateless
public class VulnerableLoggingBean implements LoggingService {
// Verwundbar: Direkte Dateiausgabe
public void logEvent(String event) {
try (PrintWriter writer = new PrintWriter(
new FileWriter("/logs/app.log", true))) {
writer.println(new Date() + ": " + event);
} catch (IOException e) {
// Logging schlägt stillschweigend fehl
}
// Logs über Cluster-Knoten verstreut
}
}
// Verwundbar: XML-Konfiguration im Konstruktor lesen
@Stateless
public class VulnerableXmlBean implements XmlService {
private Document configDoc;
@PostConstruct
public void init() {
// Verwundbar: Dateizugriff bei EJB-Initialisierung
try {
File xmlFile = new File("/config/settings.xml");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
configDoc = db.parse(xmlFile); // Kann auf einigen Servern fehlschlagen
} catch (Exception e) {
throw new RuntimeException("Kann XML-Konfiguration nicht laden", e);
}
}
}
// Verwundbar: Temporäre Dateiverwendung
@Stateless
public class VulnerableTempFileBean implements ProcessingService {
// Verwundbar: Temporäre Dateien erstellen
public byte[] processLargeData(byte[] input) {
File tempFile = null;
try {
// Verwundbar: Temp-Datei-Speicherort variiert je nach Server
tempFile = File.createTempFile("process", ".tmp");
// In Temp-Datei schreiben
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
fos.write(input);
}
// Verarbeiten...
return processFile(tempFile);
} catch (IOException e) {
throw new RuntimeException("Verarbeitung fehlgeschlagen", e);
} finally {
if (tempFile != null) {
tempFile.delete(); // Kann Dateien bei Fehler hinterlassen
}
}
}
}
Lösungscode
// Behoben: JNDI und Umgebungseinträge für Konfiguration verwenden
import javax.ejb.Stateless;
import javax.annotation.Resource;
import javax.naming.InitialContext;
@Stateless
public class SecureConfigBean implements ConfigService {
// Behoben: Container-verwaltete Umgebungseinträge verwenden
@Resource(name = "appConfig/maxConnections")
private Integer maxConnections;
@Resource(name = "appConfig/timeout")
private Integer timeout;
@Resource(name = "appConfig/serverUrl")
private String serverUrl;
// Behoben: Konfiguration aus Container-Umgebung
public ConfigDTO getConfiguration() {
ConfigDTO config = new ConfigDTO();
config.setMaxConnections(maxConnections);
config.setTimeout(timeout);
config.setServerUrl(serverUrl);
return config;
}
// Behoben: Oder JNDI-Lookup verwenden
public String getConfigValue(String key) throws NamingException {
InitialContext ctx = new InitialContext();
return (String) ctx.lookup("java:comp/env/config/" + key);
}
}
// Behoben: JPA für Datenpersistenz verwenden
import javax.ejb.Stateless;
import javax.persistence.*;
@Stateless
public class SecureDataBean implements DataService {
@PersistenceContext
private EntityManager em;
// Behoben: Daten in Datenbank über JPA speichern
public void saveRecord(RecordDTO record) {
RecordEntity entity = new RecordEntity();
entity.setId(record.getId());
entity.setContent(record.getContent());
entity.setCreatedDate(new Date());
em.persist(entity);
// Daten von jedem Server im Cluster zugänglich
}
// Behoben: Daten aus Datenbank abrufen
public RecordDTO getRecord(String id) {
RecordEntity entity = em.find(RecordEntity.class, id);
if (entity == null) {
return null;
}
RecordDTO dto = new RecordDTO();
dto.setId(entity.getId());
dto.setContent(entity.getContent());
return dto;
}
}
// Behoben: Standard-Logging-Frameworks verwenden
import javax.ejb.Stateless;
import java.util.logging.Logger;
@Stateless
public class SecureLoggingBean implements LoggingService {
// Behoben: Java-Logging oder SLF4J verwenden
private static final Logger logger =
Logger.getLogger(SecureLoggingBean.class.getName());
public void logEvent(String event) {
// Behoben: Container verwaltet Log-Ziel
logger.info("Ereignis: " + event);
// Logging-Konfiguration auf Container-Ebene behandelt
}
}
// Behoben: JNDI für XML-Konfiguration oder Datenbankspeicherung verwenden
@Stateless
public class SecureXmlBean implements XmlService {
@Resource(name = "configXml")
private String configXmlContent; // In JNDI gespeichert
@PersistenceContext
private EntityManager em;
// Behoben: XML aus Datenbank oder JNDI laden
public Document getConfiguration() throws Exception {
// Option 1: Aus JNDI-Ressource
if (configXmlContent != null) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(new InputSource(new StringReader(configXmlContent)));
}
// Option 2: Aus Datenbank
ConfigEntity config = em.find(ConfigEntity.class, "settings");
if (config != null) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(new InputSource(
new StringReader(config.getXmlContent())));
}
throw new RuntimeException("Konfiguration nicht gefunden");
}
}
// Behoben: In-Memory-Verarbeitung oder Datenbank für große Daten verwenden
@Stateless
public class SecureProcessingBean implements ProcessingService {
@PersistenceContext
private EntityManager em;
// Behoben: Im Speicher verarbeiten oder Datenbank für Staging verwenden
public byte[] processLargeData(byte[] input) {
// Für moderate Größen im Speicher verarbeiten
if (input.length < 10_000_000) { // 10MB
return processInMemory(input);
}
// Für große Daten Datenbank-BLOB verwenden
return processViaDatabase(input);
}
private byte[] processInMemory(byte[] input) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
// Ohne Temp-Dateien verarbeiten
process(new ByteArrayInputStream(input), output);
return output.toByteArray();
}
private byte[] processViaDatabase(byte[] input) {
// In Datenbank zur Verarbeitung speichern
ProcessingJob job = new ProcessingJob();
job.setInputData(input);
job.setStatus(JobStatus.PENDING);
em.persist(job);
em.flush();
// Verarbeiten (könnte async sein)
byte[] result = processLargeInput(job.getInputData());
job.setOutputData(result);
job.setStatus(JobStatus.COMPLETED);
return result;
}
}
// Behoben: Cloud-Storage für Dateibedarf verwenden
import software.amazon.awssdk.services.s3.S3Client;
@Stateless
public class SecureStorageBean implements StorageService {
@Inject
private S3Client s3Client; // Container-verwaltet
private static final String BUCKET = "app-storage";
// Behoben: Cloud-Storage von allen Knoten zugänglich verwenden
public void storeFile(String key, byte[] content) {
s3Client.putObject(
PutObjectRequest.builder()
.bucket(BUCKET)
.key(key)
.build(),
RequestBody.fromBytes(content)
);
// Von jedem Server zugänglich
}
public byte[] retrieveFile(String key) {
ResponseBytes<GetObjectResponse> response = s3Client.getObjectAsBytes(
GetObjectRequest.builder()
.bucket(BUCKET)
.key(key)
.build()
);
return response.asByteArray();
}
}
CVE-Beispiele
Keine spezifischen CVEs werden dieser CWE üblicherweise zugeordnet, da sie primär die Anwendungsportabilität und Zuverlässigkeit betrifft statt direkter Sicherheitsschwachstellen.
Referenzen
- MITRE Corporation. "CWE-576: EJB Bad Practices: Use of Java I/O." https://cwe.mitre.org/data/definitions/576.html
- Oracle. "Enterprise JavaBeans Specification."
- Jakarta EE. "Jakarta Enterprise Beans Best Practices."