sslxy

cgi-und-logfiles

Nicht Web als Oberfläche. Sondern Web als Prozess, Ausgabe, Fehler und Spur im Log.

Ein großer Teil früher Webpraxis bestand nicht aus glatten Frontends, sondern aus Dingen, die für Außenstehende kaum sichtbar waren: Shell-Accounts, Dateirechte, CGI-Verzeichnisse, Header-Ausgabe, Formularparameter, Server-Umgebungsvariablen und am Ende vor allem Logfiles. Wer in den 1990er-Jahren mit dem Web ernsthaft arbeitete, lernte sehr schnell, dass eine Seite nicht nur aus HTML bestand. Sie bestand genauso aus Zuständen, Übergaben, Fehlermeldungen und der nüchternen Frage, warum ein Server etwas gerade nicht tat.

CGI war dabei kein modischer Zusatz, sondern der praktische Weg, aus statischer Auslieferung echte Reaktion zu machen. Ein Formular kam herein, ein Skript lief, ein Header musste stimmen, eine Ausgabe musste sauber codiert werden, Rechte mussten passen, und wenn etwas schieflief, stand die Wahrheit oft nicht auf dem Bildschirm, sondern im Error-Log. Gerade dort begann die eigentliche Arbeit.

Diese Seite hält diese Schicht bewusst nüchtern fest: CGI als Übergang zwischen Webserver und Programm, typische Perl-Praxis, GET und POST, Umgebungsvariablen, Headerfehler, Access- und Error-Logs, Debugging sowie die Art von geduldiger Systemarbeit, bei der man nicht rät, sondern liest.

System Diagnostic

> CGI / LOGGING ANALYSIS
RUNTIME klassisch ein neuer externer Prozess pro Request; Webserver kontrolliert Aufruf, Umgebung, Standardinput, Standardoutput und Standardfehler INTERFACE CGI/1.1 – Meta-Variablen plus Request-Body auf STDIN; Antwort über STDOUT, Diagnose über STDERR INPUT GET, POST, QUERY_STRING, STDIN, Formulardaten, Umgebungsvariablen OUTPUT CGI-Antwortfelder wie Content-Type, Status oder Location; danach Leerzeile und optionaler Message-Body BODY CONTENT_LENGTH ist Bytezahl; CONTENT_TYPE bestimmt Parser und Zeichensatzannahmen LANGUAGE früh oft Perl, teils Shell, später auch andere Interpreter FAILURE 500er, Headerfehler, Rechtefehler, Pfadprobleme, Syntax, fehlende Module TRUTH Access-Log zeigt Request und Ergebnis; Error-Log zeigt Serverdiagnose und häufig CGI-STDERR LOG RISK URLs, Query-Strings, Referer, User-Agent und IP-Adressen können sensibel oder manipuliert sein MINDSET lesen, prüfen, eingrenzen – nicht raten
Eine gute Webphase beginnt nicht dort, wo etwas schön aussieht, sondern dort, wo sich Fehler sauber nachweisen lassen.
[cgi/overview]

CGI überhaupt: was es war und warum es wichtig war

CGI – das Common Gateway Interface – war im frühen Web der schlichte Mechanismus, mit dem ein Webserver ein externes Programm starten und dessen Ausgabe als HTTP-Antwort zurückgeben konnte. Das klingt heute fast banal. Damals war es der entscheidende Schritt vom reinen Dokumentabruf zur tatsächlich reagierenden Website.

Eine statische HTML-Datei musste nur gelesen und ausgeliefert werden. Ein CGI-Skript dagegen wurde bei einem Request tatsächlich ausgeführt. Das hieß: Parameter entgegennehmen, eventuell Daten prüfen, Inhalte zusammensetzen, vielleicht eine Mail verschicken, vielleicht eine Datei lesen oder schreiben und am Ende eine korrekte HTTP-Antwort zurückgeben. Schon damit war das Web nicht mehr nur ein Dokumentenraum, sondern eine Prozessschicht.

Gerade deshalb war CGI so prägend. Es war klein genug, um begreifbar zu sein, und direkt genug, um jeden Fehler sofort offenzulegen. Wer CGI schrieb, spürte unmittelbar, dass der Server nichts „errät“. Fehlt ein Header, ist der Header kaputt. Stimmt die Ausgabe-Reihenfolge nicht, ist die Antwort kaputt. Ist das Skript nicht ausführbar, läuft es nicht. Und wenn der Interpreter ein Problem hat, landet man im Error-Log.

[CGI Grundmodell]
> Browser sendet Request
> Webserver erkennt CGI-Ziel
> Skript/Programm wird gestartet
> Parameter werden über Umgebung / STDIN übergeben
> Skript gibt Header + Inhalt zurück

„CGI war keine Magie. Es war nur der Moment, in dem der Server statt einer Datei ein Programm laufen ließ.“

[cgi/request_lifecycle]

Vom HTTP-Request zur CGI-Antwort

  1. Verbindung und Request: Der Webserver liest Methode, Ziel, Header und gegebenenfalls einen Message-Body.
  2. URL-Zuordnung: Serverkonfiguration entscheidet, ob das Ziel statisch, CGI oder ein anderer Handler ist.
  3. Ausführungsumgebung: Server erzeugt Meta-Variablen, setzt Arbeitsverzeichnis und startet Programm beziehungsweise Interpreter.
  4. Eingabe: Request-Metadaten liegen in der Umgebung; der Body wird über Standardinput bereitgestellt.
  5. Anwendungslogik: Skript validiert, verarbeitet und erzeugt eine Antwort.
  6. Ausgabe: CGI-Header und Body gehen über Standardoutput an den Server.
  7. Diagnose: Meldungen auf Standardfehler landen je nach Serverkonfiguration im Error-Log.
  8. Abschluss: Server bildet daraus die HTTP-Antwort und schreibt Zugriffs- sowie gegebenenfalls Fehlerdaten.
[CGI_Request_Path]
> HTTP connection and request parsing
> URL mapping and authorization
> process start + meta-variables + STDIN
> CGI headers + body on STDOUT
> HTTP response + access/error logging

Der Browser spricht nicht direkt mit dem Perl-Skript. Der Webserver bleibt für Verbindung, Request-Lesen, URL-Zuordnung, Zugriffskontrolle und die endgültige HTTP-Antwort zuständig.

[cgi/execution_model]

Klassisches CGI, mod_cgi und dauerhafte Prozesse

Klassisches CGI startet für jeden Request ein neues Programm. Das schafft eine klare Zustandsgrenze: Der Prozess erhält genau seine Umgebung, verarbeitet genau diesen Request und endet wieder.

Klassisches CGI

Ein Prozess pro Request. Einfach zu verstehen und zu isolieren, aber teuer bei vielen oder schweren Aufrufen.

mod_cgi / mod_cgid

Apache-Module für CGI-Ausführung; mod_cgid verlagert das Starten externer Programme auf geeigneten Unix-MPMs in einen Daemon.

FastCGI / PSGI

Dauerhafte Anwendungsprozesse vermeiden wiederholten Interpreterstart, müssen aber Speicher, globalen Zustand und Request-Trennung sauber behandeln.

Statisch

Kein Anwendungsprozess pro Abruf. Wo keine Eingabe- oder Zustandslogik benötigt wird, bleibt das die kleinste Angriffs- und Fehlerfläche.

„Dauerhaft“ ist nicht automatisch sicherer oder einfacher. Klassisches CGI bezahlt mit Prozesskosten; persistente Systeme bezahlen mit langlebigem Zustand, Speicherverwaltung und komplexerer Isolation.

[runtime/environment]

Shell-Account, Serverpfade und Laufzeitumgebung

Wer frühe CGI-Praxis nur aus heutiger Framework-Sicht betrachtet, unterschätzt leicht, wie nah das Ganze an der Server- und Benutzerumgebung hing. Man arbeitete nicht in einer abstrahierten Plattform, sondern in einem realen Dateibaum mit Benutzerrechten, Gruppenrechten, Skriptpfaden, Shebang-Zeilen und konkreten Interpreter-Versionen.

Der Shell-Account war dabei oft nicht bloß Bequemlichkeit, sondern Voraussetzung. Man musste Dateien hochladen, Rechte setzen, Skripte direkt aufrufen, vielleicht lokal mit der Shell testen, Logfiles lesen, Verzeichnisse durchsuchen und herausfinden, ob ein Problem im Skript, im Pfad, in den Rechten oder in der Serverkonfiguration lag. Das war keine getrennte DevOps-Welt. Das gehörte schlicht zur Webarbeit dazu.

  • cgi-bin oder freigeschaltete CGI-Verzeichnisse: nicht jede Datei durfte einfach ausgeführt werden.
  • Shebang: der Interpreterpfad musste stimmen, sonst lief das Skript nicht oder falsch.
  • Dateirechte: Lesbarkeit allein reichte nicht – das Skript musste vom Serverprozess sinnvoll ausführbar sein.
  • Pfadlogik: relative und absolute Pfade waren reale Fehlerquellen, besonders beim Zugriff auf Includes, Templates oder Datendateien.
  • Umgebung: REQUEST_METHOD, QUERY_STRING, CONTENT_LENGTH, REMOTE_ADDR, HTTP_USER_AGENT und ähnliche Variablen waren echte Arbeitsbasis.

Diese Nähe zur Infrastruktur war lehrreich. Man sah sehr schnell, dass ein Webprozess keine abstrakte Oberfläche ist, sondern ein ausgelöster Systemvorgang unter konkreten Bedingungen. Wer das einmal sauber begriffen hatte, verlor meistens die Lust an mystifizierten Erklärungen darüber, warum „der Server heute irgendwie spinnt“.

[cgi/meta_variables]

Meta-Variablen: Vertrag zwischen Server und Skript

Variable Bedeutung und Vorsicht
GATEWAY_INTERFACE CGI-Schnittstellenversion, typischerweise CGI/1.1.
REQUEST_METHOD HTTP-Methode; nicht ungeprüft auf GET und POST beschränken, wenn der Server weitere Methoden durchreicht.
QUERY_STRING Unverarbeiteter Query-Teil ohne führendes Fragezeichen; vollständig untrusted.
CONTENT_TYPE Medientyp des Request-Bodys einschließlich möglicher Parameter wie Charset oder Multipart-Boundary.
CONTENT_LENGTH Dezimale Anzahl der Bytes, die das Skript vom Standardinput lesen darf.
SCRIPT_NAME URL-Pfadanteil, der das Skript identifiziert.
PATH_INFO Zusätzlicher URL-Pfad hinter dem Skript; muss getrennt von Dateisystempfaden behandelt werden.
SERVER_NAME / SERVER_PORT Serveridentität und Port nach Serverkonfiguration; bei Proxybetrieb nicht automatisch öffentliche Ursprungs-URL.
REMOTE_ADDR Netzwerkadresse des direkten Gegenübers; hinter Reverse Proxy häufig Proxyadresse.
REMOTE_USER Vom Server authentisierter Benutzer, sofern eine entsprechende Zugriffsauthentisierung aktiv war.
HTTP_* Aus Client-Headern abgeleitete Werte wie HTTP_USER_AGENT oder HTTP_REFERER; niemals als vertrauenswürdig behandeln.
[cgi/script_path_info]

SCRIPT_NAME, PATH_INFO und Dateisystempfade

Bei einer URL wie /cgi-bin/item.pl/gruppe/42 bezeichnet SCRIPT_NAME typischerweise das Skript, während PATH_INFO den zusätzlichen Pfadteil enthält. Diese Trennung erlaubte früh sprechendere CGI-URLs.

PATH_INFO ist jedoch kein geprüfter lokaler Dateiname. Punkte, Schrägstriche, Kodierung und Normalisierung können zwischen URL-, Server- und Betriebssystemebene unterschiedlich behandelt werden.

  • URL-Pfad dekodieren: nur nach klar definierter Reihenfolge und niemals mehrfach unkontrolliert.
  • Erlaubte Segmente festlegen: interne IDs besser gegen eine Allowlist beziehungsweise ein enges Format prüfen.
  • Dateipfade selbst erzeugen: keine Benutzerpfade direkt an open, unlink oder Shellkommandos weiterreichen.
  • Basisverzeichnis festhalten: nach Normalisierung prüfen, dass das Ziel weiterhin innerhalb des vorgesehenen Bereichs liegt.
[http/headers]

Header zuerst: warum kleinste Ausgabefehler alles stoppen

Einer der frühesten und wichtigsten Lerneffekte bei CGI war die Tatsache, dass die Antwort formal beginnen musste. Bei einer normalen Dokumentantwort liefert das Skript mindestens Content-Type; fehlt ein eigener Status-Header, wird üblicherweise Status 200 angenommen. Nach den Headerfeldern folgt eine leere Zeile und erst danach der optionale Message-Body.

Für heutige Systeme klingt das trivial. In der frühen Praxis war es eine konstante Fehlerquelle. Ein zusätzliches Leerzeichen an falscher Stelle, eine Warnung vor dem Header, eine kaputte Zeichenausgabe, ein Modulhinweis, ein Debug-Print zu früh – und schon war die Antwort aus Sicht des Servers beschädigt. Der Browser zeigte oft nur einen generischen Fehler. Das Error-Log hingegen war meist präziser.

#!/usr/bin/perl
use strict;
use warnings;
binmode STDOUT, ':raw';
print "Content-Type: text/html; charset=UTF-8\r\n\r\n";
print "<!doctype html><html lang=\"de\">...";

Gerade daran lernte man ein frühes Gefühl für Protokolldisziplin. Das Problem war selten „CGI allgemein“. Das Problem war meistens sehr konkret: falscher Header, Header zu spät, Zeichensatzfrage, ungewollte Ausgabe, fehlende Leerzeile oder ein Skriptabbruch, bevor überhaupt ein Header zurückgegeben wurde.

[cgi/response_types]

Dokumentantwort, Status und Redirect

Antwort Typische Felder
Dokument Content-Type, optional Status und weitere erlaubte Response-Header, danach Body.
Fehlerstatus Status: 400 Bad Request oder anderer Status plus Content-Type und erklärender Body.
Client-Redirect Location mit absoluter URI; Server erzeugt daraus die Weiterleitungsantwort.
Lokaler Redirect Location mit lokalem Pfad; Server verarbeitet das neue Ziel intern nach seiner Konfiguration.
Status: 400 Bad Request
Content-Type: text/plain; charset=UTF-8
 
Ungültige Formulardaten.

Ein CGI-Skript sollte erwartbare Benutzerfehler nicht als unkontrollierten Prozessabbruch behandeln. Eine definierte 4xx-Antwort ist diagnostisch und betrieblich klarer als ein generischer 500er.

[http/input]

GET, POST und die tatsächliche Eingaberealität

Die begriffliche Trennung zwischen GET und POST war früh klar, aber in der Praxis musste man sie wirklich verstehen, nicht nur benennen. GET übergibt Parameter im Query-Teil der URL – sichtbar, speicherbar und leicht bookmarkbar. POST trägt Daten gewöhnlich im Request-Body. Das verändert Semantik und Transportform, schafft aber ohne TLS keinerlei Vertraulichkeit.

Methode Praktische Eigenschaft
GET Für sichere, abrufende Operationen und teilbare URLs geeignet. Query-Daten können in Verlauf, Lesezeichen, Referer und Logs erscheinen.
POST Für Verarbeitung oder Zustandsänderung geeignet. Body-Daten stehen nicht im normalen URL-Feld, können aber von Anwendung, Proxy oder Diagnosewerkzeugen protokolliert werden.

Das Entscheidende lag jedoch nicht nur in der Methode selbst, sondern in der Verarbeitung. URL-Decoding, Pluszeichen als Leerzeichen, Prozentkodierung, Sonderzeichen, Zeilenumbrüche, Zeichensatzprobleme – all das musste das Skript sauber behandeln. Wer dabei nachlässig war, bekam keine abstrakte Theoriediskussion, sondern kaputte Formulardaten, abgeschnittene Felder oder später sogar Sicherheitsprobleme.

[CGI Input Modell]
> REQUEST_METHOD prüfen
> bei GET: QUERY_STRING lesen
> bei Body: CONTENT_TYPE prüfen + CONTENT_LENGTH als Bytezahl begrenzen
> exakt die erlaubte Bytezahl von STDIN lesen
> medientypspezifisch dekodieren, syntaktisch und semantisch validieren
[http/content_types]

application/x-www-form-urlencoded und multipart/form-data

Medientyp Verarbeitung
application/x-www-form-urlencoded Schlüssel-Wert-Paare mit Prozentkodierung; Plus wird in Formularsemantik häufig als Leerzeichen interpretiert.
multipart/form-data Mehrteiliger Body mit Boundary; notwendig für reguläre Dateiuploads und komplexere Formularteile.
text/plain Für HTML-Formulare technisch möglich, aber mehrdeutig und für zuverlässige strukturierte Verarbeitung ungeeignet.
application/json Keine klassische HTML-Formularcodierung, aber bei moderneren CGI-Endpunkten möglich; eigenen Parser und klare Größen-/Typgrenzen verwenden.

Das Skript darf nicht unabhängig vom CONTENT_TYPE denselben Parser verwenden. Besonders Multipart-Daten sollten mit einer bewährten Bibliothek verarbeitet werden; selbstgeschriebene Boundary-Parser sind fehleranfällig.

[cgi/input_parsing]

Dekodierung, Mehrfachwerte und Zeichensätze

Ein Parametername kann mehrfach vorkommen: tag=unix&tag=perl. Ein Parser muss deshalb festlegen, ob er eine Liste, den ersten oder den letzten Wert liefert. Stillschweigende Unterschiede zwischen Bibliotheken können Sicherheitsprüfungen und Anwendungslogik auseinanderlaufen lassen.

  • Rohdaten zuerst begrenzen: maximale Body- und Feldgröße vor vollständiger Verarbeitung festlegen.
  • Einmal dekodieren: mehrfache Prozentdekodierung kann Prüfungen umgehen.
  • Namen und Werte trennen: für jedes Feld ein erwartetes Format und eine maximale Länge definieren.
  • Mehrfachwerte bewusst behandeln: keine zufällige „erste oder letzte gewinnt“-Logik.
  • Unicode normalisieren, wenn fachlich nötig: sichtbare Gleichheit und Bytegleichheit sind nicht dasselbe.
  • Ausgabekontext beachten: Validierung ersetzt nicht HTML-, Header-, Shell- oder Datenbank-spezifisches Escaping.
[cgi/perl]

Perl-CGI in der frühen Praxis

Frühes CGI war oft praktisch gleichbedeutend mit Perl. Nicht weil Perl die einzige Möglichkeit gewesen wäre, sondern weil es auf vielen Systemen vorhanden, textstark, pragmatisch und gut genug für genau diese Übergangsschicht war. Formulare lesen, Strings zerlegen, Mail zusammensetzen, Dateien öffnen, Datum und Zeit formatieren, kleinere Zustände verwalten – all das ließ sich in Perl mit vergleichsweise wenig Aufwand umsetzen.

Gleichzeitig war Perl kein Schutzraum. Wenn das Skript schlampig war, war das Ergebnis schlampig. Wenn Variablen unklar geführt wurden, wurde die Fehleranalyse unangenehm. Wenn reguläre Ausdrücke schlecht saßen oder Eingaben blind übernommen wurden, entstanden echte Probleme. Gerade deshalb war frühe Perl-Praxis für viele lehrreich: nicht wegen Eleganz im akademischen Sinn, sondern weil sich die Folgen jeder Nachlässigkeit unmittelbar zeigten.

Was gut funktionierte

Stringverarbeitung, kleine Webformulare, Mail-Dispatch, Zustandsdateien, schnelle Prototypen, klare Textausgabe.

Wo es unangenehm wurde

unsaubere Eingabeprüfung, Dateirechte, Shell-Aufrufe, Zeichensatzfragen, nicht abgefangene Warnungen, schwer lesbar gewordene Ein-Datei-Skripte.

Wer mit Perl-CGI arbeitete, lernte außerdem etwas Wichtiges über Webdynamik: Der eigentliche Aufwand lag selten im sichtbaren HTML. Der Aufwand lag in korrekter Übergabe, validen Zuständen und reproduzierbarer Fehlersuche. Genau deshalb waren Access- und Error-Logs kein Nebenthema, sondern permanenter Bestandteil der Arbeit.

[perl/safe_practice]

Perl-Skripte nachvollziehbar und defensiv halten

#!/usr/bin/perl -T
use strict;
use warnings;
local $ENV{PATH} = '/usr/bin:/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
# Eingaben anschließend gegen erlaubte Formate prüfen.
  • strict und warnings: machen Variablenfehler und verdächtige Zustände früher sichtbar.
  • Taint Mode: kann über -T zusätzliche Prüfungen für fremd beeinflusste Daten aktivieren, ist aber kein Ersatz für Validierung oder ein Berechtigungsmodell.
  • Expliziter PATH: externe Programme nicht über eine unkontrollierte Suchumgebung finden lassen.
  • Dreiargumentiges open: Modus und Dateiname getrennt halten; Benutzerdaten nicht als magische Pipe-Syntax interpretieren lassen.
  • List-Form statt Shell: externe Programme möglichst ohne Shell und mit getrennten Argumenten aufrufen.
  • Ausgabe kontextgerecht codieren: HTML, Mailheader, Dateiname und Shellargument brauchen unterschiedliche Regeln.
[cgi/forms]

Formulare, Mail-Skripte und die Realität dahinter

Für viele war CGI zuerst über Formulare sichtbar. Kontaktformulare, einfache Bestellanfragen, Gästebuch-Einträge, kleine Suchmasken oder Konfigurationsübergaben – all das lief im Kern auf dieselbe Frage hinaus: Wie kommen Benutzereingaben kontrolliert auf dem Server an und was passiert dann damit? Browserseitige Pflichtfelder und JavaScript waren dabei nur Bedienhilfe; die maßgebliche Prüfung musste serverseitig erfolgen.

In der frühen Praxis war das häufig ein Mail-Skript. Der Browser schickte ein Formular, CGI nahm Felder entgegen, prüfte sie mehr oder weniger gut und setzte daraus eine E-Mail oder eine lokale Datendatei zusammen. Technisch simpel, praktisch aber voller Fallstricke: fehlende Feldprüfung, ungefilterte Sonderzeichen, kaputte Newlines, Header-Manipulation, falsche Rechte auf temporären Dateien oder ein Sendmail-Aufruf, der lokal anders funktionierte als gedacht.

  • Pflichtfelder: nicht nur im Browser markieren, sondern serverseitig wirklich prüfen.
  • Zeilenumbrüche: harmlose Textfelder konnten Mails und Dateiformate schnell unlesbar machen, wenn man sie blind übernahm.
  • Mailversand: Empfänger fest konfigurieren, Headerzeilenumbrüche aus Benutzereingaben ablehnen und möglichst eine Mailbibliothek statt zusammengesetzter Shellbefehle verwenden.
  • Missbrauchsschutz: Größen-, Zeit- und Frequenzgrenzen sowie nachvollziehbare Fehlerantworten einplanen.
  • Dankeseiten und Redirects: mussten als HTTP-Antwort technisch ebenso korrekt gebaut werden wie jede normale HTML-Seite.

Gerade daran trennte sich frühe Webromantik von tatsächlicher Webarbeit. Ein Formular war eben nicht „fertig“, nur weil es hübsch erschien. Es war erst dann brauchbar, wenn Eingaben sauber entgegengenommen, protokolliert, geprüft und nachvollziehbar verarbeitet wurden.

[cgi/files_state]

Dateien, Sperren, temporäre Daten und atomare Updates

Gästebücher, Zähler und einfache Formularablagen schrieben häufig direkt in Dateien. Genau dort entstanden Nebenläufigkeits- und Rechteprobleme: zwei gleichzeitige Prozesse konnten denselben Stand lesen und einander anschließend überschreiben.

  • Sperren: bei gemeinsam veränderten Dateien passende exklusive beziehungsweise gemeinsame Locks verwenden.
  • Temporäre Dateien: sicher erzeugen, nicht aus erratbaren Namen im allgemein beschreibbaren Verzeichnis zusammensetzen.
  • Atomarer Austausch: vollständigen neuen Stand schreiben, synchronisieren und anschließend innerhalb desselben Dateisystems umbenennen.
  • Umask und Rechte: neu erzeugte Dateien nur den tatsächlich benötigten Konten zugänglich machen.
  • Keine Webauslieferung: Rohdaten, Mailwarteschlangen und Zustandsdateien außerhalb öffentlich erreichbarer Verzeichnisse halten.
  • Symlinks beachten: privilegierte oder anders berechtigte Prozesse dürfen nicht auf vom Angreifer umleitbare Pfade schreiben.

Ein erfolgreiches open bedeutete noch nicht, dass ein Mehrbenutzerbetrieb korrekt war. CGI machte Nebenläufigkeit selbst bei kleinen Seiten zu einer realen Systemfrage.

[cgi/process_cost]

Prozessstart, Interpreterkosten und Ressourcen

Bei klassischem CGI werden Interpreter, Module und Programmlogik für jeden Request neu gestartet. Für gelegentliche Formulare war das oft akzeptabel; bei hohem Verkehr oder großen Abhängigkeiten wurde es teuer.

Ursache Auswirkung und Behandlung
Interpreterstart CPU- und Speicheraufwand je Request; kleine Skripte und wenige Module helfen.
Externe Programme Weitere Prozesse, Wartezeiten und zusätzliche Fehlerpfade.
Langsame Clients Request-Lesezeiten müssen auf Serverebene begrenzt werden.
Große Bodies Vor dem Einlesen server- und anwendungsseitig begrenzen.
Dauerhafte Alternativen FastCGI/PSGI sparen Startkosten, verlangen aber saubere Request-Trennung und Lebenszyklusverwaltung.
[logs/access]

Access-Log: wer kam, wann und womit

Das Access-Log war die rohe Spur des Verkehrs. Dort stand nicht die philosophische Bedeutung eines Zugriffs, sondern ganz praktisch: wann etwas angefordert wurde, von welcher direkten Netzwerkadresse, mit welcher Methode, auf welches Ziel, mit welchem finalen Statuscode, in welcher Größe und – je nach Format – mit welchem Referer oder User-Agent. Hinter Proxy oder Loadbalancer ist die protokollierte Gegenstelle nicht automatisch der ursprüngliche Benutzer.

Für frühe Webarbeit war das enorm nützlich. Man sah, ob eine Seite wirklich aufgerufen wurde, ob ein CGI-Endpunkt 200 oder 500 lieferte, ob ein Formular mit GET statt POST ankam, ob ein Bot über alte Pfade lief oder ob jemand immer wieder dieselbe defekte URL abfragte. Das Access-Log war damit kein Statistikspielzeug, sondern Betriebsprotokoll.

127.0.0.1 - - [12/Mar/1998:19:14:08 +0100] "GET /cgi-bin/test.pl?x=1 HTTP/1.0" 200 482
203.0.113.10 - - [12/Mar/1998:19:15:02 +0100] "POST /cgi-bin/formmail.pl HTTP/1.0" 500 213
198.51.100.44 - - [12/Mar/1998:19:16:41 +0100] "GET /nonexistent.cgi HTTP/1.0" 404 178

Gerade in Kombination mit dem Error-Log wurde daraus eine brauchbare Arbeitsmethode: Im Access-Log sah man, dass etwas passiert war. Im Error-Log sah man oft, warum es scheiterte.

[logs/common_combined]

Common Log Format und Combined Log Format

Apache kann das Zugriffsformat über LogFormat festlegen. Das klassische Common Log Format enthält Gegenstelle, Identität, Benutzer, Zeitpunkt, Requestzeile, finalen Status und übertragene Bytes.

%h %l %u %t "%r" %>s %b

Das Combined Log Format ergänzt Referer und User-Agent:

%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"
Token Bedeutung
%h Remote Host beziehungsweise Adresse nach Serverauflösung.
%l Identd-Wert; in der Praxis meist Bindestrich.
%u Authentisierter Remote-Benutzer oder Bindestrich.
%t Zeitpunkt des Requests im konfigurierten Logformat.
%r Erste Requestzeile mit Methode, Ziel und Protokollversion.
%>s Finaler Status nach internen Weiterleitungen.
%b Antwortgröße ohne Header; Bindestrich bei null Bytes.

Logfelder sind Beobachtungen, keine automatisch verifizierten Tatsachen. Referer und User-Agent stammen gewöhnlich direkt aus Client-Headern.

[logs/status_codes]

Statuscodes als erste Klassifikation

Klasse Diagnostische Einordnung
2xx Request wurde verarbeitet; fachlicher Inhalt kann trotzdem falsch oder unvollständig sein.
3xx Weiterleitung oder Bedingungsantwort; Ziel und Redirect-Schleifen prüfen.
4xx Request, Berechtigung oder Ressource passt nicht; 404 kann echter Altlink oder automatischer Scan sein.
5xx Server- oder Anwendungsfehler; Error-Log und CGI-STDERR unmittelbar daneben auswerten.

Ein 200er beweist nur, dass der Server eine erfolgreiche Antwort geliefert hat. Er beweist nicht, dass eine Mail zugestellt, ein Datensatz dauerhaft geschrieben oder eine fachliche Transaktion korrekt abgeschlossen wurde.

[logs/error]

Error-Log: wo die Wahrheit über Fehler stand

Das Error-Log war für CGI meistens der eigentliche Arbeitsplatz. Browserfehler waren oft unpräzise. Eine leere Seite, ein 500 Internal Server Error oder einfach nur eine kaputte Antwort sagten wenig. Das Error-Log dagegen sagte häufig das Entscheidende: Syntaxfehler, fehlende Module, falscher Interpreterpfad, unzulässige Rechte, Header-Probleme, Warnungen vor dem Header, Datei nicht gefunden, uninitialisierte Variablen oder Shell-Aufrufe, die nicht durchliefen.

Wer ernsthaft mit CGI arbeitete, gewöhnte sich früh an diese Reihenfolge: Nicht zuerst den Browser anstarren, sondern das Error-Log lesen. Dort lag meistens die eigentliche Wahrheit. Und wenn dort nichts stand, war das auch eine Information – dann musste man die Logkonfiguration, die Rechte oder den tatsächlichen Ausführungspfad prüfen.

[Fri Mar 12 19:15:02 1998] [error] Premature end of script headers: formmail.pl
[Fri Mar 12 19:15:02 1998] [error] syntax error at /home/user/cgi-bin/formmail.pl line 47
[Fri Mar 12 19:18:11 1998] [error] malformed header from script. Bad header=Hello: test.pl
[logs/cgi_stderr]

STDERR, Warnungen und Request-Zuordnung

Ein CGI-Programm sollte Diagnosemeldungen auf Standardfehler und die eigentliche CGI-Antwort auf Standardoutput schreiben. Vermischen sich beide Kanäle, können Warnungen vor dem Header eine ansonsten richtige Antwort zerstören.

warn "request_id=$request_id invalid field=name\\n";
print "Status: 400 Bad Request\\r\\n";
print "Content-Type: text/plain; charset=UTF-8\\r\\n\\r\\n";
print "Ungültige Eingabe.\\n";
  • Kontext protokollieren: Skript, Phase, Fehlerklasse und eine nicht geheime Request-Kennung.
  • Keine Passwörter: Authentisierungsdaten, vollständige Formulare und Session-Geheimnisse nicht in Diagnosemeldungen schreiben.
  • Zeit korrelieren: Access- und Error-Log nach Zeitpunkt, Prozess-/Thread- und Request-Kennung verbinden.
  • Benutzerfehler trennen: erwartbare 4xx-Fälle nicht als unverständliche Perl-Warnung behandeln.
[logs/privacy_minimization]

IP-Adressen, URLs und Formulardaten nicht gedankenlos sammeln

Access- und Anwendungslogs können IP-Adressen, Zeitpunkte, aufgerufene Pfade, Query-Strings, Referer, User-Agent, Benutzernamen und fachliche Vorgänge zusammenführen. Schon eine URL kann Suchbegriffe, interne IDs oder persönliche Angaben enthalten.

  • Zweck festlegen: Betrieb, Sicherheit und Fehleranalyse von bloßer Neugier trennen.
  • Query-Strings prüfen: Passwörter, Tokens und persönliche Inhalte gehören nicht in URLs.
  • Felder minimieren: nur protokollieren, was für den definierten Zweck benötigt wird.
  • Zugriff begrenzen: Logs nicht als allgemein lesbare Textdateien behandeln.
  • Aufbewahrung begrenzen: Rotation und Löschung nach nachvollziehbarem Plan.
  • Export bereinigen: Beispieldaten und Archivkopien anonymisieren beziehungsweise pseudonymisieren.

Die konkrete rechtliche Bewertung gehört in die aktuelle Datenschutz- und Betriebsdokumentation der Domain, nicht in eine historische CGI-Anleitung.

[logs/integrity_injection]

Logs selbst sind eine Eingabeschnittstelle

Requestziele und Header können vom Client beeinflusst sein. Rohlogs können deshalb Steuerzeichen, täuschende Zeilenumbrüche, Terminalsequenzen oder absichtlich irreführende Texte enthalten.

  • Rohlogs vorsichtig anzeigen: nicht blind in privilegierte Terminal- oder HTML-Ausgaben übernehmen.
  • Strukturiert parsen: Felder nicht nur an Leerzeichen zerlegen, wenn Anführungszeichen und Escape-Regeln beteiligt sind.
  • Untrusted markieren: Referer, User-Agent, Host und Query bleiben fremde Daten.
  • Geheimnisse redigieren: Tokens, Session-IDs und Passwörter vor zentraler Sammlung entfernen.
  • Integrität schützen: Logverzeichnisse dürfen nicht durch Web- oder Skriptbenutzer beliebig austauschbar sein.
[logs/rotation_retention]

Rotation, Wiederöffnung und Aufbewahrung

Ein laufender Webserver hält Logdateien geöffnet. Nur eine Datei umzubenennen oder zu löschen garantiert deshalb nicht, dass der Prozess sofort in eine neue Datei schreibt.

  1. Rotieren: aktuelle Datei nach festem Größen- oder Zeitplan umbenennen beziehungsweise über ein Rotationsprogramm führen.
  2. Neu öffnen: Webserver kontrolliert beziehungsweise „graceful“ zum Wiederöffnen der Logs veranlassen.
  3. Komprimieren: erst geschlossene ältere Dateien komprimieren.
  4. Rechte erhalten: neue Logdateien mit korrektem Eigentümer und restriktivem Modus erzeugen.
  5. Aufbewahren und löschen: Betriebs-, Sicherheits- und Datenschutzanforderungen gemeinsam berücksichtigen.
  6. Rotation überwachen: volles Dateisystem und stillstehende Rotation selbst protokollieren oder alarmieren.

Apache kann Logs auch an ein Rotationsprogramm weiterleiten. Dieses Modell vermeidet manuelle Signalfolgen, erhöht aber die Abhängigkeit von einem weiteren laufenden Prozess.

[debug/practice]

Debugging ohne Komfortschicht

Frühe CGI-Arbeit bedeutete oft Debugging ohne die Bequemlichkeiten späterer Entwicklungsumgebungen. Kein hübscher Live-Inspector, kein eingebauter Stacktrace im Browser, kein vollintegriertes Observability-System. Stattdessen: Skript lokal aus der Shell aufrufen, Testparameter manuell setzen, Header-Ausgaben kontrollieren, temporäre Debug-Ausgaben einbauen, Logfiles lesen und dann die Kette aus Request, Skript und Antwort gedanklich sauber nachverfolgen.

Gerade diese Reduktion hatte einen Vorteil: Man lernte, die Schichten zu trennen. Läuft das Skript überhaupt? Kommt der Interpreter hoch? Sind die Variablen gesetzt? Ist die Methode korrekt? Ist die Content-Length als Bytezahl plausibel und innerhalb des Limits? Kommt vor dem Header ungewollte Ausgabe? Ist ein Dateipfad relativ falsch? Ist ein Modul vorhanden? Jede Frage griff in eine andere Ebene – und genau so musste man sie behandeln.

Typischer Ablauf

Reproduzieren, Access-Log prüfen, Error-Log lesen, Skript direkt testen, Eingabedaten isolieren, Ausgabeformat kontrollieren, Rechte und Pfade verifizieren.

Typische Haltung

Weniger Vermutung, mehr Eingrenzung. Weniger Oberfläche, mehr tatsächlicher Laufweg. Genau dort wurde frühe Webarbeit ernst.

„Debugging hieß oft nicht, auf eine IDE zu warten. Es hieß lesen, was wirklich passiert war.“

[debug/shell_reproduction]

CGI ohne Browser reproduzierbar testen

Ein Skript kann in einer kontrollierten Shellumgebung mit denselben Kernvariablen und Eingabebytes getestet werden. Dadurch trennt man Parser- und Programmlogik vom Webserver.

# GET-Test
REQUEST_METHOD=GET QUERY_STRING='name=Test' ./hello.cgi

# POST-Test mit exakt berechneter Bytezahl
body='name=Test&topic=CGI'
length=$(printf '%s' "$body" | wc -c | tr -d ' ')
printf '%s' "$body" | REQUEST_METHOD=POST \\
CONTENT_TYPE='application/x-www-form-urlencoded' \\
CONTENT_LENGTH="$length" ./form.cgi

Der Test muss mit derselben Interpreterversion, demselben Benutzerkontext, denselben Dateirechten und möglichst derselben Umgebung erfolgen. Ein Skript, das als eigener Shell-Benutzer funktioniert, kann unter dem Webserverkonto weiterhin an Rechten oder Pfaden scheitern.

Prüfreihenfolge

  1. Syntax: Interpreter-Compilecheck ohne Requestdaten.
  2. Header: erste Bytes der Ausgabe kontrollieren.
  3. GET: einfacher bekannter Query-String.
  4. POST: Body, Content-Type und exakte Bytezahl.
  5. Grenzfälle: leere, doppelte, zu lange und ungültig codierte Werte.
  6. Serverlauf: erst danach über reale URL und Logs prüfen.
[cgi/security]

Frühe Sicherheitsprobleme und naive Skripte

Frühes CGI war nicht nur praktisch, sondern auch berüchtigt für naive Lösungen. Gerade Mail-Skripte, Gästebücher, Suchmasken, Dateioperationen oder Shell-nahe Hilfsskripte wurden oft schnell geschrieben und schlecht abgesichert. Das war nicht bloß ein historischer Schönheitsfehler. Ein CGI-Programm kann grundsätzlich mit allen Rechten des ausführenden Serverkontos auf Systemressourcen zugreifen, die dieses Konto erreichen darf.

Typische Probleme waren ungeprüfte Eingaben, blindes Durchreichen an Shell-Aufrufe, Header-Injection in Mail-Skripten, Dateipfade mit unzureichender Kontrolle, schreibbare Verzeichnisse ohne klare Trennung oder schlicht ein zu großes Vertrauen in das, was „der Benutzer schon nicht absichtlich falsch machen wird“.

  • Serverseitige Eingabeprüfung: Syntax und fachliche Bedeutung möglichst früh und positiv gegen erwartete Formate prüfen.
  • Shell-Nähe vermeiden: strukturierte APIs und argumentgetrennte Prozessaufrufe statt zusammengesetzter Kommandozeilen verwenden.
  • Ausgabe codieren: fremde Texte niemals ungefiltert in HTML, Header, JavaScript oder Logs einbauen.
  • Offene Formmail-Skripte: beliebige Empfänger und Header machten sie zu Spam-Werkzeugen.
  • Rechte minimieren: Skripte und Daten mit dem kleinsten erforderlichen Benutzer- und Dateizugriff betreiben.
  • Abhängigkeiten pflegen: Interpreter, Module und Webserver nicht als eingefrorene historische Insel öffentlich betreiben.

Genau deshalb blieb von dieser Zeit nicht nur Webromantik, sondern auch ein dauerhafter Reflex: lieber weniger Dynamik, wenn sie nicht wirklich gebraucht wird; lieber klare serverseitige Prüfung; lieber statische Auslieferung dort, wo keine Prozesslogik nötig ist. Wer frühe CGI-Fehler gesehen hatte, brauchte keine Theorie darüber, warum unnötige Dynamik unnötige Risiken mitbringt.

[security/threat_model]

Typische Angriffsklassen eines CGI-Endpunkts

Klasse Technische Ursache und Abwehr
Command Injection Benutzerdaten werden Teil eines Shellkommandos; Shell vermeiden, Argumente trennen, Allowlist und geringste Rechte verwenden.
Header Injection CR/LF aus Formularwerten erzeugen zusätzliche Mail- oder HTTP-Header; Zeilenumbrüche ablehnen und Headerbibliotheken verwenden.
Path Traversal Benutzerpfad verlässt das erlaubte Verzeichnis; IDs statt Pfade, kanonisieren und Basisgrenze prüfen.
Cross-Site Scripting Fremde Daten werden ohne kontextgerechte HTML-Ausgabecodierung zurückgegeben.
CSRF Authentisierte oder zustandsändernde Operation wird durch fremde Seite ausgelöst; Token und geeignete Same-Site-/Origin-Prüfung verwenden.
Upload-Missbrauch Dateityp, Größe, Inhalt, Name oder Speicherort werden nicht begrenzt; außerhalb des Webroots speichern und verifizieren.
Resource Exhaustion Große Bodies, langsame Requests, teure Regexe oder endlose externe Prozesse; Limits und Timeouts setzen.
Secret Leakage Passwörter, Tokens, Umgebungen oder Formulardaten gelangen in HTML oder Logs; Redaction und Minimalprotokollierung.
[security/resource_limits]

Größen-, Zeit- und Prozessgrenzen

Ein CGI-Skript muss nicht kompromittiert sein, um einen Server zu überlasten. Eine formal gültige Eingabe kann zu groß, zu langsam oder zu rechenintensiv sein.

  • Request-Zeile und Header: serverseitige Längen- und Feldzahlgrenzen.
  • Request-Body: Maximalgröße vor dem Einlesen und zusätzlich pro Feld beziehungsweise Upload.
  • Request-Lesezeit: langsame Clients nicht unbegrenzt Ressourcen binden lassen.
  • Skriptlaufzeit: externe Programme, Netzwerkzugriffe und Parser mit Timeouts versehen.
  • Ausgabegröße: keine unbegrenzten Dumps oder Endlosschleifen in der Response.
  • Parallelität: Prozesszahl, Warteschlangen und Dateisperren überwachen.

Limits gehören auf mehrere Ebenen: Webserver, Betriebssystem, CGI-Programm und gegebenenfalls nachgelagerter Dienst.

[mindset/log-reading]

Warum Logfiles die eigentliche Schule waren

Rückblickend liegt ein wesentlicher Wert dieser Zeit nicht einmal nur in CGI selbst, sondern im Umgang mit Logfiles. Access- und Error-Logs zwangen zu einer Nüchternheit, die später oft verloren ging. Dort stand kein Marketing, kein freundliches Narrativ, keine weichgespülte Zusammenfassung. Dort stand, was passiert war.

Man sah Requests, Fehlercodes, Pfade, Zeitpunkte, Methoden, Warnungen, Headerprobleme und die Folgen kleinster Ausgabefehler. Genau dadurch lernte man, Systeme nicht als Story zu lesen, sondern als Ereignisketten. Diese Art zu denken bleibt nützlich – nicht nur im Web, sondern überall dort, wo Technik nachvollziehbar und wartbar bleiben soll.

Wer so gelernt hat, entwickelt fast automatisch eine Skepsis gegen unnötige Schichten, die das tatsächliche Verhalten verdecken. Nicht weil moderne Werkzeuge grundsätzlich schlecht wären, sondern weil gute Technik lesbar bleiben sollte. Ein Access-Log oder Error-Log ist brutal ehrlich. Gerade das ist sein Wert.

„Logfiles loben nichts. Genau deshalb sind sie brauchbar.“

[archive/cgi_preservation]

Frühe CGI-Systeme nachvollziehbar archivieren

Ein altes Skript allein reicht nicht. Die Ausführung hing von Webserver, Interpreter, Modulen, Dateirechten, Verzeichnisstruktur, Mailtransport und Konfiguration ab.

  • Quelltext: unverändertes Original und bereinigte Lesefassung getrennt sichern.
  • Interpreter: Sprache, Version, Shebang und benötigte Module dokumentieren.
  • Serverkonfiguration: ScriptAlias, Handler, Umgebungsvariablen, Benutzerkontext und relevante Limits erhalten.
  • Dateibaum: Templates, Datendateien, Hilfsprogramme und erwartete Arbeitsverzeichnisse mit aufnehmen.
  • Beispielrequests: GET-, POST- und Fehlerfälle als reproduzierbare Testdaten festhalten.
  • Logs: nur notwendige Ausschnitte sichern und persönliche beziehungsweise geheime Daten vorher bereinigen.
  • Mail und externe Dienste: historische Abhängigkeiten dokumentieren, aber beim Test nicht unkontrolliert real auslösen.
  • Isolierte Rekonstruktion: alte Skripte in einer abgeschotteten Testumgebung statt direkt im öffentlichen Web untersuchen.
[documentation/cgi_sources]

Technische Referenzen

CGI/1.1 wird durch RFC 3875 als dokumentierte „current practice“ beschrieben. Die Webserver- und Logabschnitte orientieren sich zusätzlich an der offiziellen Apache-HTTP-Server-Dokumentation; Perl- und Sicherheitsabschnitte an Perl- und OWASP-Unterlagen.

RFC 3875 ist eine informative Dokumentation der CGI/1.1-Praxis, kein Internetstandard. Webserver und Betriebssysteme können einzelne system- oder implementierungsabhängige Details unterschiedlich behandeln.

[cgi/conclusion]

Was davon geblieben ist

CGI war nicht perfekt, nicht elegant in jedem Detail und sicher nicht frei von naiven Konstruktionen. Aber es hatte eine Stärke, die man ernst nehmen sollte: Es machte den Weg zwischen Request, Ausführung und Antwort sichtbar. Gerade deshalb war es eine gute Schule.

Wer mit CGI, Shell-Accounts und Logfiles gearbeitet hat, lernte früh, dass Webentwicklung mehr ist als sichtbares Layout. Sie ist Protokoll, Prozess, Eingabe, Ausgabe, Rechte, Pfadlogik, Nebenläufigkeit, Datenschutz und Fehleranalyse. Genau daraus kommt ein Teil der späteren Vorliebe für ruhige, statische, nachvollziehbare Lösungen.

Die Host- und Account-Seite darunter beschreibt shell-accounts-und-hosts.htm. Die spätere Haltung gegenüber Struktur und Werkzeugen steht auf philosophy.htm. Und die Linie der frühen Webpraxis insgesamt bleibt auf webmaster.htm sichtbar.

[CGI] in einem Satz
> Request wird Prozess
> Prozess wird Ausgabe
> Fehler landen im Log
> wer lesen kann, versteht das System