Sicherheitsrichtlinien

Verbindliche Sicherheitsstandards für WvdS FPC RAD Suite, konform mit KRITIS/NIS2.

Diese Richtlinien sind nicht verhandelbar. Code, der sie verletzt, wird nicht akzeptiert.

OWASP Top 10 Prävention

CWE-78: Command Injection

Risiko: Ausführung beliebiger Befehle durch manipulierten Input.

// VERBOTEN - Unsicher!
Exec('cmd /c ' + UserInput);
Shell('fpc ' + ProjectPath + ' ' + UserArgs);
 
// KORREKT - Parameterisiert
Options.Shell := False;
Args := TStringArray.Create;
Args.Add(ProjectPath);
Spawn('fpc', Args, Options);

Maßnahmen:

  • Immer shell: false verwenden
  • Argumente als Array übergeben
  • Pfade validieren vor Verwendung

CWE-22: Path Traversal

Risiko: Zugriff auf Dateien außerhalb des erlaubten Verzeichnisses.

// VERBOTEN - Unsicher!
FileName := BasePath + UserInput;
ReadFile(FileName);
 
// KORREKT - Validierung
function IsPathSafe(const ABasePath, AUserInput: string): Boolean;
var
  ResolvedPath: string;
begin
  // Keine .. erlauben
  if Pos('..', AUserInput) > 0 then
    Exit(False);
 
  ResolvedPath := ExpandFileName(Concat(ABasePath, AUserInput));
  Result := Pos(ABasePath, ResolvedPath) = 1;
end;
 
if IsPathSafe(BasePath, UserInput) then
  ReadFile(Concat(BasePath, UserInput));

Maßnahmen:

  • .. Sequenzen blockieren
  • Pfade normalisieren und validieren
  • Basis-Pfad muß Präfix des Ergebnis-Pfads sein

CWE-20: Input Validation

Risiko: Ungültige Daten führen zu Fehlfunktion oder Angriff.

// VERBOTEN - Keine Validierung
procedure CreateProject(const AName: string);
begin
  MakeDir(AName);  // Was wenn AName = '../../../etc'?
end;
 
// KORREKT - Validierung
function ValidateProjectName(const AName: string; out AError: string): Boolean;
const
  ALLOWED_CHARS = ['a'..'z', 'A'..'Z', '0'..'9', '_', '-'];
  MAX_LENGTH = 64;
var
  I: Integer;
begin
  Result := False;
 
  if AName = '' then
  begin
    AError := rsProjectNameEmpty;
    Exit;
  end;
 
  if Length(AName) > MAX_LENGTH then
  begin
    AError := Format(rsProjectNameTooLong, [MAX_LENGTH]);
    Exit;
  end;
 
  for I := 1 to Length(AName) do
    if not (AName[I] in ALLOWED_CHARS) then
    begin
      AError := Format(rsProjectNameInvalidChar, [AName[I]]);
      Exit;
    end;
 
  Result := True;
end;

CWE-316: Cleartext Credentials

Risiko: Credentials im Code oder in Logs.

// VERBOTEN - Credentials im Code
const
  API_TOKEN = 'ghp_xxxxxxxxxxxx';
  DB_PASSWORD = 'secret123';
 
// VERBOTEN - Credentials loggen
LogDebug('Token: %s', [Token]);
 
// KORREKT - Environment Variables
Token := GetEnvironmentVariable('GITHUB_TOKEN');
if Token = '' then
  raise EWvdSConfigError.Create(rsTokenNotConfigured);
 
// KORREKT - Maskiertes Logging
LogDebug('Token configured: %s', [BoolToStr(Token <> '', True)]);

CWE-532: Log Injection

Risiko: Sensible Daten in Logs sichtbar.

// VERBOTEN
LogInfo('User login: %s with password: %s', [User, Password]);
LogDebug('API response: %s', [FullResponse]);  // Kann Tokens enthalten!
 
// KORREKT
LogInfo('User login: %s', [User]);  // Kein Password
LogDebug('API response received, length: %d', [Length(Response)]);

CWE-79: Cross-Site Scripting (XSS)

Risiko: Einschleusen von Script-Code in WebViews.

// VERBOTEN - Unescaped HTML
WebView.Html := '<div>' + UserInput + '</div>';
 
// KORREKT - Escaping
function EscapeHtml(const AText: string): string;
begin
  Result := AText;
  Result := StringReplace(Result, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
  Result := StringReplace(Result, '>', '&gt;', [rfReplaceAll]);
  Result := StringReplace(Result, '"', '&quot;', [rfReplaceAll]);
  Result := StringReplace(Result, '''', '&#39;', [rfReplaceAll]);
end;
 
WebView.Html := '<div>' + EscapeHtml(UserInput) + '</div>';

CWE-20: WebView Message Whitelist

Risiko: Unbekannte Message-Typen ermöglichen Angriffe auf WebView-Handler.

Jeder WebView-Message-Handler MUSS eine Whitelist erlaubter Message-Typen implementieren.
// VERBOTEN - Kein Message-Type-Check
procedure HandleMessage(AMessage: TJSObject);
var
  MsgType: string;
begin
  MsgType := string(AMessage['type']);
  case MsgType of
    'browse': HandleBrowse(AMessage);
    'save': HandleSave(AMessage);
  end;
end;
 
// KORREKT - Mit Whitelist
const
  ALLOWED_MESSAGE_TYPES: array[0..6] of string = (
    'browse', 'save', 'cancel', 'validatePath',
    'autoDetect', 'config', 'pathSelected'
  );
 
function IsAllowedMessageType(const AType: string): Boolean;
var
  I: Integer;
begin
  Result := False;
  for I := Low(ALLOWED_MESSAGE_TYPES) to High(ALLOWED_MESSAGE_TYPES) do
    if ALLOWED_MESSAGE_TYPES[I] = AType then
      Exit(True);
end;
 
procedure HandleMessage(AMessage: TJSObject);
var
  MsgType: string;
begin
  MsgType := string(AMessage['type']);
 
  // Whitelist-Prüfung ZUERST
  if not IsAllowedMessageType(MsgType) then
  begin
    LogWarning(rsUnknownMessageType, [MsgType]);
    Exit;
  end;
 
  case MsgType of
    'browse': HandleBrowse(AMessage);
    'save': HandleSave(AMessage);
    // ...
  end;
end;

Maßnahmen:

  • Whitelist mit allen erlaubten Message-Typen definieren
  • Unbekannte Typen loggen und ablehnen
  • Bei neuen Funktionen Whitelist aktualisieren

Defensive Node.js API-Aufrufe

Risiko: Node.js APIs können je nach Kontext unterschiedliche Methoden haben.

Problem aus Praxis: stdout.setEncoding ist keine Funktion wenn Process nicht korrekt initialisiert ist.
// VERBOTEN - Direkte Methodenaufrufe ohne Check
procedure SetupProcessHandlers;
begin
  asm
    this.FProcess.stdout.setEncoding('utf8');
    this.FProcess.stderr.setEncoding('utf8');
  end;
end;
 
// KORREKT - Defensive typeof-Checks
procedure SetupProcessHandlers;
begin
  asm
    if (this.FProcess && this.FProcess.stdout) {
      if (typeof this.FProcess.stdout.setEncoding === 'function') {
        this.FProcess.stdout.setEncoding('utf8');
      }
      if (typeof this.FProcess.stdout.on === 'function') {
        this.FProcess.stdout.on('data', this.HandleStdout.bind(this));
      }
    }
    if (this.FProcess && this.FProcess.stderr) {
      if (typeof this.FProcess.stderr.setEncoding === 'function') {
        this.FProcess.stderr.setEncoding('utf8');
      }
      if (typeof this.FProcess.stderr.on === 'function') {
        this.FProcess.stderr.on('data', this.HandleStderr.bind(this));
      }
    }
  end;
end;

Maßnahmen:

  • typeof … === 'function' vor jedem Methodenaufruf
  • Objektexistenz prüfen (if (obj && obj.property))
  • Fehlende APIs graceful behandeln

Error Handling

Keine leeren Exception-Handler

// VERBOTEN
try
  DoSomething;
except
  // Fehler verschlucken
end;
 
// KORREKT
try
  DoSomething;
except
  on E: ESpecificError do
  begin
    LogError(rsSpecificError, [E.Message]);
    // Behandlung...
  end;
  on E: Exception do
  begin
    LogError(rsUnexpectedError, [E.ClassName, E.Message]);
    raise;  // Oder angemessen behandeln
  end;
end;

Spezifische Exception-Typen

// KORREKT - Spezifische Exceptions
try
  Result := DoOperation;
except
  on E: EFileNotFoundException do
    HandleFileNotFound(E.FileName);
  on E: EAccessDenied do
    HandleAccessDenied(E.Path);
  on E: ENetworkError do
    HandleNetworkError(E.Url);
  on E: Exception do
    HandleUnexpectedError(E);
end;

Debug-Logging

Debug-Logging erfordert zwei Bedingungen:

  1. Compile-Zeit: {$IFDEF DEBUG}
  2. Laufzeit: –debug Parameter
{$IFDEF DEBUG}
var
  DebugLogFile: TextFile;
  DebugEnabled: Boolean;
 
procedure InitDebugLogging;
begin
  DebugEnabled := ParamStr(1) = '--debug';
  if DebugEnabled then
  begin
    AssignFile(DebugLogFile, Format('debug-%s.log',
      [FormatDateTime('yymmddhhnnss', Now)]));
    Rewrite(DebugLogFile);
  end;
end;
 
procedure LogDebugTrace(const AMessage: string; const AArgs: array of const);
begin
  if not DebugEnabled then Exit;
 
  WriteLn(DebugLogFile, Format('[%s] %s',
    [FormatDateTime('hh:nn:ss.zzz', Now), Format(AMessage, AArgs)]));
  Flush(DebugLogFile);
end;
{$ENDIF}

Was darf geloggt werden

Erlaubt Verboten
Dateinamen, Pfade Tokens, API-Keys
Aktionsnamen Paßwörter
Numerische IDs Session-IDs
Fehlermeldungen Vollständige Requests/Responses
Konfigurationskeys Konfigurationswerte (sensibel)

Sichere Defaults

// Sichere Standardwerte
const
  DEFAULT_TIMEOUT = 30000;  // 30 Sekunden
  MAX_FILE_SIZE = 10 * 1024 * 1024;  // 10 MB
  MAX_RECURSION_DEPTH = 100;
 
type
  TSpawnOptions = record
    Shell: Boolean;        // Default: False (sicher)
    Timeout: Integer;      // Default: 30000
    WorkingDir: string;
    Environment: TStringArray;
  end;
 
function DefaultSpawnOptions: TSpawnOptions;
begin
  Result.Shell := False;  // WICHTIG: Kein Shell!
  Result.Timeout := DEFAULT_TIMEOUT;
  Result.WorkingDir := '';
  Result.Environment := nil;
end;

Code Review Checkliste

Vor Merge MUSS geprüft werden:

SICHERHEIT:
[ ] Keine hardcodierten Credentials
[ ] Alle Benutzereingaben validiert
[ ] Keine Shell-Injection möglich
[ ] Keine Path-Traversal möglich
[ ] HTML wird escaped in WebViews
[ ] Keine sensiblen Daten in Logs

ERROR HANDLING:
[ ] Keine leeren Exception-Handler
[ ] Spezifische Exceptions behandelt
[ ] Fehler werden geloggt (ohne sensible Daten)

KONFIGURATION:
[ ] Sichere Defaults verwendet
[ ] Timeouts definiert
[ ] Limits definiert (Dateigröße, Rekursion)

Sicherheits-Audit

Regelmäßige Prüfung mit automatisierten Tools:

wvds-lint Sicherheits-Checks

wvds-lint security --path sources/

Geprüft wird:

  • Exec/Shell-Aufrufe
  • Path-Operationen ohne Validierung
  • Hardcodierte Strings (potentielle Credentials)
  • Leere Exception-Handler
  • Log-Aufrufe mit sensiblen Parametern

Manuelle Review

Bei jeder neuen Extension:

  • Alle Entry-Points prüfen
  • Alle externen Inputs tracen
  • WebView-Kommunikation prüfen

Incident Response

Bei Entdeckung einer Sicherheitslücke:

  1. Nicht veröffentlichen bis Patch bereit
  2. Issue auf GitHub mit Label „security“ (privat)
  3. Patch entwickeln und testen
  4. Neue Version releasen
  5. Advisory veröffentlichen

Siehe auch

Zuletzt geändert: den 29.01.2026 um 15:13