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.