Corona-Warn-App und Datenbank-Sicherheit

Alvar Freude hat sich den Datenbank-Code der Corona-Warn-App mal genauer angeschaut. Dieser zeigt beispielhaft, was bei vielen Datenbank-Projekten schieflaufen kann. Darüberhinaus zeigt er auch, wie wir selbst solche Fehler vermeiden können.

Alvar Freude ist Referent für technischen Datenschutz und Informationsfreiheit beim LfDI Baden-Württemberg. Zudem ist er Autor u.a. von PostgreSQL Secure Monitoring (Posemo) und TLS-Check. Er analysierte den Datenbank-Code des corona-warn-app Server und veröffentlichte seine Erkenntnisse auf Twitter.

Im Folgenden sind hier seine Tweets nochmal zusammengefasst und ergänzt:

Datenbank-Berechtigungen

Eines der wesentlichen Ergebnisse ist, dass die Datenbank-Berechtigungen viel zu weitgehend sind, und ein erfolgreicher Angreifer auf alle Daten zugreifen, diese löschen usw. könnte. Den Einwand, dass ein Angreifer nie soweit kommen dürfe widerlegt er mit der Erfahrung, dass dies leider immer wieder passiere.

Das PostgreSQL-Login per Superuser postgres sollte daher immer nur über Unix-Domain-Sockets und über localhost erlaubt sein. Der Zugang mit Peer-Authentifizierung in der pg_hba.conf-Datei ist hingegen ok:

# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             postgres                                peer
host    all             all             10.23.42.1/24           scram-sha-256

Da die Anwendung einen Datenbank-Superuser erhält, jedoch keine Datenbank konfiguriert wird, wird diese wohl automatisch angelegt. Dies hieße jedoch, dass der Prozess als Superuser ausgeführt werden müsste – eine ganz schlechte Idee. Die Datenbank sollte vom DBA mit Superuser-Rechten angelegt und anschließend so konfiguriert werden, dass sich nicht jeder (PUBLIC) damit verbinden kann:

CREATE DATABASE cwa;
REVOKE ALL ON cwa FROM PUBLIC;

Damit kann sich nur noch der Superuser mit der Datenbank cwa verbinden.

Falls eine Tabelle, wie z.B. V1__createTables.sql jedoch zunächst normal angelegt wurde, d.h. PUBLIC alle Rechte hat, sollten diese entzogen werden mit:

REVOKE ALL ON diagnosis_key FROM PUBLIC;
REVOKE ALL ON diagnosis_key FROM current_user;

Anschließend könnt ihr eine Rolle cwa_users mit den beiden Nutzern cwa_reader und cwa_inserter erstellen um zwischen lesenden und schreibenden Zugriffen unterscheiden zu können:

CREATE ROLE cwa_users;
CREATE USER cwa_reader IN ROLE cwa_users PASSWORD '…';
CREATE USER cwa_inserter IN ROLE cwa_users  PASSWORD '…';

Nun erhält die Rolle cwa_users zunächst CONNECT-Rechte und dann cwa_reader Lese- und cwa_inserter Hinzufügen-Rechte:

GRANT CONNECT ON DATABASE to cwa_users;
GRANT SELECT ON diagnosis_key TO cwa_reader;
GRANT INSERT ON diagnosis_key TO cwa_inserter;

Der cwa_reader-User kann jedoch damit zunächst alle Daten auf einmal lesen. Da dies jedoch nicht erforderlich ist, kann dieser Angriffspunkt durch eine Funktion beschnitten werden:

CREATE OR REPLACE FUNCTION get_key_data(in_id UUID)
    RETURNS JSONB
    AS 'SELECT key_data FROM diagnosis_key WHERE id = in_id;'
    LANGUAGE sql SECURITY DEFINER SET search_path = :schema, pg_temp;

Anschließend wird die Funktion cwa_owner zugewiesen, cwa_reader und cwa_inserter die Berechtigungen entzogen und schließlich die Ausführung der Funktion cwa_reader erlaubt:

ALTER FUNCTION get_key_data(UUID) OWNER TO cwa_owner;
REVOKE ALL ON FUNCTION get_key_dataUUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION get_key_data(UUID) TO cwa_reader;

Damit kann cwa_reader also nur noch einen einzelnen Datensatz lesen.

Passwörter speichern

Auch die in .env genannten Standardpasswörter haben für Alvar Freude gute Chancen, auch in der Produktion erhalten zu bleiben.

Daher sollten beim Anlegen der Users sichere Passwörter vergeben werden, die anschließend in Vault oder ähnlichem gespeichert werden sollten.

id

Interessant in der Tabellen-Definition ist auch, dass die id als bigserial realisiert ist. Eine hochzählende Zahl könnte jedoch von Angreifern erraten werden. Daher dürfte der UUIDv4-Datentyp besser geeignet sein. Zur UUIDv4-Generierung kommt entweder die uuid-ossp-Erweiterung oder für PostgreSQL≥9.4 auch die pgcrypto-Erweiterung infrage, also entweder:

CREATE EXTENSION "uuid-ossp";
CREATE TABLE diagnosis_key (
  id uuid primary key default uuid_generate_v4() NOT NULL,
  …
);

oder:

CREATE EXTENSION "pgcrypto";
CREATE TABLE diagnosis_key (
  id uuid primary key default gen_random_uuid() NOT NULL,
  …
);

Zeitstempel

In der Tabellen-Definition von V1__createTables.sql werden Datum und Zeit in submission_timestamp als bigint, also als Zahl, gespeichert, und dies obwohl es auch einen TIMESTAMP-Datentyp gibt. Dies hätte den Vorteil, dass mit ihnen auch gerechnet werden kann, also z.B.:

SELECT age(submission_timestamp);
SELECT submission_timestamp - '1 day'::interval;

Außerdem könnten die Daten nach einer bestimmten Zeit gelöscht werden, z.B. nach dreißig Tagen mit:

DELETE FROM diagnosis_key WHERE age(submission_timestamp) > 30;

Das Löschen kann noch beschleunigt werden, wenn für jeden Tag mit der PostgreSQL-Erweiterung pg_partman eine eigene Partition erstellt wird.