Linee guida sulla sicurezza

Standard di sicurezza vincolanti per la suite WvdS FPC RAD, conformi a KRITIS/NIS2.

Queste linee guida sono non negoziabili. Il codice che le viola non sara accettato.

Prevenzione OWASP Top 10

CWE-78: Command Injection

Rischio: Esecuzione di comandi arbitrari tramite input manipolato.

// VIETATO - Non sicuro!
Exec('cmd /c ' + UserInput);
Shell('fpc ' + ProjectPath + ' ' + UserArgs);
 
// CORRETTO - Parametrizzato
Options.Shell := False;
Args := TStringArray.Create;
Args.Add(ProjectPath);
Spawn('fpc', Args, Options);

Misure:

  • Usare sempre shell: false
  • Passare argomenti come array
  • Validare i percorsi prima dell'uso

CWE-22: Path Traversal

Rischio: Accesso a file al di fuori della directory consentita.

// VIETATO - Non sicuro!
FileName := BasePath + UserInput;
ReadFile(FileName);
 
// CORRETTO - Validazione
function IsPathSafe(const ABasePath, AUserInput: string): Boolean;
var
  ResolvedPath: string;
begin
  // Non permettere ..
  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));

Misure:

  • Bloccare sequenze ..
  • Normalizzare e validare i percorsi
  • Il percorso base deve essere prefisso del percorso risultante

CWE-20: Input Validation

Rischio: Dati non validi causano malfunzionamenti o attacchi.

// VIETATO - Nessuna validazione
procedure CreateProject(const AName: string);
begin
  MakeDir(AName);  // E se AName = '../../../etc'?
end;
 
// CORRETTO - Validazione
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: Credenziali in chiaro

Rischio: Credenziali nel codice o nei log.

// VIETATO - Credenziali nel codice
const
  API_TOKEN = 'ghp_xxxxxxxxxxxx';
  DB_PASSWORD = 'secret123';
 
// VIETATO - Loggare credenziali
LogDebug('Token: %s', [Token]);
 
// CORRETTO - Variabili d'ambiente
Token := GetEnvironmentVariable('GITHUB_TOKEN');
if Token = '' then
  raise EWvdSConfigError.Create(rsTokenNotConfigured);
 
// CORRETTO - Logging mascherato
LogDebug('Token configurato: %s', [BoolToStr(Token <> '', True)]);

CWE-532: Log Injection

Rischio: Dati sensibili visibili nei log.

// VIETATO
LogInfo('Login utente: %s con password: %s', [User, Password]);
LogDebug('Risposta API: %s', [FullResponse]);  // Puo contenere token!
 
// CORRETTO
LogInfo('Login utente: %s', [User]);  // Nessuna password
LogDebug('Risposta API ricevuta, lunghezza: %d', [Length(Response)]);

CWE-79: Cross-Site Scripting (XSS)

Rischio: Iniezione di codice script nelle WebView.

// VIETATO - HTML non escapato
WebView.Html := '<div>' + UserInput + '</div>';
 
// CORRETTO - 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: Whitelist messaggi WebView

Rischio: Tipi di messaggio sconosciuti permettono attacchi ai gestori WebView.

Ogni gestore messaggi WebView DEVE implementare una whitelist dei tipi di messaggio consentiti.
// VIETATO - Nessun controllo tipo messaggio
procedure HandleMessage(AMessage: TJSObject);
var
  MsgType: string;
begin
  MsgType := string(AMessage['type']);
  case MsgType of
    'browse': HandleBrowse(AMessage);
    'save': HandleSave(AMessage);
  end;
end;
 
// CORRETTO - Con 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']);
 
  // Controllo whitelist PRIMA
  if not IsAllowedMessageType(MsgType) then
  begin
    LogWarning(rsUnknownMessageType, [MsgType]);
    Exit;
  end;
 
  case MsgType of
    'browse': HandleBrowse(AMessage);
    'save': HandleSave(AMessage);
    // ...
  end;
end;

Misure:

  • Definire whitelist con tutti i tipi di messaggio consentiti
  • Loggare e rifiutare tipi sconosciuti
  • Aggiornare la whitelist per nuove funzionalita

Chiamate API Node.js difensive

Rischio: Le API Node.js possono avere metodi diversi a seconda del contesto.

Problema dalla pratica: stdout.setEncoding non e una funzione se il processo non e inizializzato correttamente.
// VIETATO - Chiamate metodo dirette senza controllo
procedure SetupProcessHandlers;
begin
  asm
    this.FProcess.stdout.setEncoding('utf8');
    this.FProcess.stderr.setEncoding('utf8');
  end;
end;
 
// CORRETTO - Controlli typeof difensivi
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;

Misure:

  • typeof … === 'function' prima di ogni chiamata metodo
  • Verificare esistenza oggetto (if (obj && obj.property))
  • Gestire gracefully API mancanti

Gestione errori

Nessun handler eccezione vuoto

// VIETATO
try
  DoSomething;
except
  // Ingoiare l'errore
end;
 
// CORRETTO
try
  DoSomething;
except
  on E: ESpecificError do
  begin
    LogError(rsSpecificError, [E.Message]);
    // Gestione...
  end;
  on E: Exception do
  begin
    LogError(rsUnexpectedError, [E.ClassName, E.Message]);
    raise;  // O gestire appropriatamente
  end;
end;

Tipi eccezione specifici

// CORRETTO - Eccezioni specifiche
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

Il debug logging richiede due condizioni:

  1. Compile-time: {$IFDEF DEBUG}
  2. Runtime: parametro –debug
{$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}

Cosa si puo loggare

Consentito Vietato
Nomi file, percorsi Token, API key
Nomi azioni Password
ID numerici ID sessione
Messaggi errore Request/response completi
Chiavi configurazione Valori configurazione (sensibili)

Default sicuri

// Valori default sicuri
const
  DEFAULT_TIMEOUT = 30000;  // 30 secondi
  MAX_FILE_SIZE = 10 * 1024 * 1024;  // 10 MB
  MAX_RECURSION_DEPTH = 100;
 
type
  TSpawnOptions = record
    Shell: Boolean;        // Default: False (sicuro)
    Timeout: Integer;      // Default: 30000
    WorkingDir: string;
    Environment: TStringArray;
  end;
 
function DefaultSpawnOptions: TSpawnOptions;
begin
  Result.Shell := False;  // IMPORTANTE: Nessuna shell!
  Result.Timeout := DEFAULT_TIMEOUT;
  Result.WorkingDir := '';
  Result.Environment := nil;
end;

Checklist code review

Prima del merge DEVE essere verificato:

SICUREZZA:
[ ] Nessuna credenziale hardcoded
[ ] Tutti gli input utente validati
[ ] Nessuna shell injection possibile
[ ] Nessun path traversal possibile
[ ] HTML escapato nelle WebView
[ ] Nessun dato sensibile nei log

GESTIONE ERRORI:
[ ] Nessun handler eccezione vuoto
[ ] Eccezioni specifiche gestite
[ ] Errori loggati (senza dati sensibili)

CONFIGURAZIONE:
[ ] Default sicuri usati
[ ] Timeout definiti
[ ] Limiti definiti (dimensione file, ricorsione)

Audit sicurezza

Verifica regolare con strumenti automatizzati:

Controlli sicurezza wvds-lint

wvds-lint security --path sources/

Viene verificato:

  • Chiamate Exec/Shell
  • Operazioni path senza validazione
  • Stringhe hardcoded (potenziali credenziali)
  • Handler eccezione vuoti
  • Chiamate log con parametri sensibili

Review manuale

Per ogni nuova estensione:

  • Verificare tutti gli entry point
  • Tracciare tutti gli input esterni
  • Verificare comunicazione WebView

Risposta agli incidenti

In caso di scoperta di una vulnerabilita:

  1. Non pubblicare finche la patch non e pronta
  2. Issue su GitHub con label „security“ (privata)
  3. Sviluppare e testare la patch
  4. Rilasciare nuova versione
  5. Pubblicare advisory

Vedi anche

Zuletzt geändert: il 29/01/2026 alle 22:34