#!/usr/bin/env python3 """ Zugangssperre und Webroot. Diese Gruppe prueft nicht die Anwendung, sondern das, was vor ihr steht: die .htaccess. Sie steht am Anfang jedes Laufs, weil eine offene Testinstanz ein groesseres Problem waere als jeder Befund darin - und weil ein Deployment die .htaccess neu schreibt, also jedes Mal die Gelegenheit hat, sie kaputt zu machen. """ import base64 from harness import test, erwarte, erwarte_status, erwarte_fehler @test('sperre', 'Startseite ohne Zugangsdaten abgewiesen') def _(ctx): """Ohne Basic-Auth darf die Testinstanz nichts ausliefern.""" r = ctx.client.get('/', basic=False, token=None) erwarte_status(r, 401) @test('sperre', 'Startseite mit falschem Passwort abgewiesen') def _(ctx): """Ein falsches Passwort darf nicht durchrutschen - sonst waere die Datei kaputt.""" falsch = base64.b64encode(b'deploy:definitiv-falsch').decode() r = ctx.client.get('/', basic=False, token=None, headers={'Authorization': 'Basic ' + falsch}) erwarte_status(r, 401) @test('sperre', 'Startseite mit Zugangsdaten erreichbar') def _(ctx): """Mit den richtigen Daten muss das WebUI antworten, sonst sperrt die Datei zu viel.""" r = ctx.client.get('/', token=None) erwarte_status(r, 200, 302) @test('sperre', 'Konfiguration nicht abrufbar') def _(ctx): """config/config.php enthaelt Zugangsdaten der Instanz.""" for pfad in ('/config/config.php', '/config/.htpasswd', '/config/config.example.php'): r = ctx.client.get(pfad, token=None) erwarte_status(r, 403) @test('sperre', 'Anwendungscode nicht abrufbar') def _(ctx): """src/, sql/ und cli/ sind kein Webinhalt.""" for pfad in ('/src/Core/Config.php', '/src/bootstrap.php', '/sql/schema.sql', '/sql/migrations/016_roadmap.sql', '/cli/tick.php', '/cli/roadmap.php'): r = ctx.client.get(pfad, token=None) erwarte_status(r, 403) @test('sperre', 'Laufzeitverzeichnis nicht abrufbar') def _(ctx): """In var/log/ stehen Fehlermeldungen samt Pfaden.""" for pfad in ('/var/log/', '/var/.htaccess'): r = ctx.client.get(pfad, token=None) erwarte_status(r, 403) @test('sperre', 'Serverdaten und Deployment-Konfiguration nicht vorhanden') def _(ctx): """Was gar nicht erst hochgeladen wird, kann auch nicht gelesen werden.""" for pfad in ('/Serverdaten.txt', '/scripts/deploy_config.json', '/scripts/deploy_dev_config.json', '/tests/server/testctl.php'): r = ctx.client.get(pfad, token=None) erwarte_status(r, 403, 404) @test('sperre', 'Interne Dokumentation nicht abrufbar') def _(ctx): """ docs/ im Repository ist nicht public/docs/. Dort stehen interne Adressen und Wege, die Instanz zurueckzusetzen. Erreichbar ist es nicht, weil /docs/... auf public/docs/ umgeschrieben wird - eine Wirkung, die eine Aenderung an den Routen versehentlich aufheben koennte. """ for pfad in ('/docs/TESTINSTANZ.md', '/docs/TESTUMGEBUNG.md', '/docs/ROADMAP.md'): r = ctx.client.get(pfad, token=None) erwarte_status(r, 403, 404) @test('sperre', 'Oeffentliche Dokumentation weiterhin erreichbar') def _(ctx): """Die Gegenprobe: public/docs/ soll die Sperre nicht mitgesperrt haben.""" r = ctx.client.get('/docs/bugtracker.md', token=None) erwarte_status(r, 200) @test('sperre', 'Kein Verzeichnisindex') def _(ctx): """Options -Indexes: Verzeichnisse duerfen ihren Inhalt nicht auflisten.""" for pfad in ('/public/assets/', '/public/docs/release-template/'): r = ctx.client.get(pfad, token=None) erwarte(r.status in (403, 404) or 'Index of' not in (r.body or ''), "%s liefert einen Verzeichnisindex" % pfad) @test('sperre', 'API ohne Token bleibt hinter der Sperre') def _(ctx): """Die Ausnahme in der .htaccess greift nur mit Token-Header, nie ohne.""" r = ctx.client.get('/api/health', basic=False, token=None) erwarte_status(r, 401) erwarte('WWW-Authenticate' in ' '.join(r.headers.keys()) or 'www-authenticate' in r.headers, "401 ohne WWW-Authenticate - das ist die Anwendung, nicht die Sperre") @test('sperre', 'API mit Token-Header wird durchgelassen') def _(ctx): """Mit X-Agent-Token darf ein SDK die API ohne Basic-Auth erreichen.""" r = ctx.client.get('/api/health', basic=False) erwarte(r.status != 401 or r.fehlercode is not None, "Die .htaccess laesst /api/ mit Token-Header nicht durch (HTTP %d)" % r.status) erwarte_status(r, 200, 503) @test('sperre', 'API mit Bearer wird durchgelassen') def _(ctx): """Der zweite Weg, ein Token zu uebergeben - vom .NET-SDK genutzt.""" r = ctx.client.get('/api/health', basic=False, bearer=True) erwarte_status(r, 200, 503) @test('sperre', 'Token-Header oeffnet nur /api/') def _(ctx): """Ein Token-Header darf nicht das WebUI oder die Doku freischalten.""" for pfad in ('/', '/index.php', '/login.php', '/docs/', '/install_db.php', '/testctl.php'): r = ctx.client.get(pfad, basic=False) erwarte_status(r, 401) @test('sperre', 'Sicherheitskopfzeilen gesetzt') def _(ctx): """nosniff, Frame-Options und Referrer-Policy kommen aus der .htaccess.""" r = ctx.client.get('/api/health') erwarte(r.headers.get('x-content-type-options') == 'nosniff', "X-Content-Type-Options fehlt oder ist %r" % r.headers.get('x-content-type-options')) r2 = ctx.client.get('/', token=None) fehlend = [h for h in ('x-frame-options', 'referrer-policy') if h not in r2.headers] erwarte(not fehlend, "Fehlende Kopfzeilen auf der Startseite: %s" % ', '.join(fehlend)) @test('sperre', 'testctl ohne Shared Key abgewiesen') def _(ctx): """Die Basic-Auth allein darf nicht genuegen, um Tabellen zu loeschen.""" r = ctx.client.post('/testctl.php', body={'action': 'status'}, token=None) erwarte_fehler(r, http=401, code='unauthorized') @test('sperre', 'testctl mit falschem Shared Key abgewiesen') def _(ctx): r = ctx.client.post('/testctl.php', body={'action': 'status'}, token='dc_master_falsch') erwarte_fehler(r, http=401, code='unauthorized') @test('sperre', 'testctl loescht nicht ohne Bestaetigung') def _(ctx): """Ohne den Namen der Datenbank in confirm darf nichts verschwinden.""" r = ctx.testctl('wipe') erwarte_fehler(r, http=400, code='confirmation_required') r2 = ctx.testctl('wipe', confirm='irgendwas-anderes') erwarte_fehler(r2, http=400, code='confirmation_required') # Und die Tabellen stehen noch. r3 = ctx.testctl('status') erwarte(r3.feld('tables', default=0) > 20, "Nach den abgewiesenen Loeschversuchen fehlen Tabellen: %s" % r3.kurz(200))