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

AuswirkungDetails
SonstigeBereich: Sonstige

Qualitätsverschlechterung - Anwendungen werden nicht portabel und können in geclusterten oder verteilten Deployment-Umgebungen fehlschlagen.
VerfügbarkeitBereich: 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

  1. MITRE Corporation. "CWE-576: EJB Bad Practices: Use of Java I/O." https://cwe.mitre.org/data/definitions/576.html
  2. Oracle. "Enterprise JavaBeans Specification."
  3. Jakarta EE. "Jakarta Enterprise Beans Best Practices."