====== Sigurnosne smjernice ====== Obavezni sigurnosni standardi za WvdS FPC RAD Suite, sukladni s KRITIS/NIS2. Ove smjernice su **nepregovarajuće**. Kod koji ih krši neće biti prihvaćen. ===== OWASP Top 10 Prevencija ===== ==== CWE-78: Command Injection ==== **Rizik:** Izvršavanje proizvoljnih naredbi putem manipuliranog inputa. // ZABRANJENO - Nesigurno! Exec('cmd /c ' + UserInput); Shell('fpc ' + ProjectPath + ' ' + UserArgs); // ISPRAVNO - Parametrizirano Options.Shell := False; Args := TStringArray.Create; Args.Add(ProjectPath); Spawn('fpc', Args, Options); **Mjere:** * Uvijek koristiti ''shell: false'' * Argumente predavati kao niz * Validirati putanje prije korištenja ==== CWE-22: Path Traversal ==== **Rizik:** Pristup datotekama izvan dozvoljenog direktorija. // ZABRANJENO - Nesigurno! FileName := BasePath + UserInput; ReadFile(FileName); // ISPRAVNO - Validacija function IsPathSafe(const ABasePath, AUserInput: string): Boolean; var ResolvedPath: string; begin // Ne dozvoliti .. 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)); **Mjere:** * Blokirati ''..'' sekvence * Normalizirati i validirati putanje * Bazna putanja mora biti prefiks rezultantne putanje ==== CWE-20: Input Validation ==== **Rizik:** Nevaljani podaci dovode do neispravnog rada ili napada. // ZABRANJENO - Bez validacije procedure CreateProject(const AName: string); begin MakeDir(AName); // Što ako AName = '../../../etc'? end; // ISPRAVNO - Validacija 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 ==== **Rizik:** Credentials u kodu ili u logovima. // ZABRANJENO - Credentials u kodu const API_TOKEN = 'ghp_xxxxxxxxxxxx'; DB_PASSWORD = 'secret123'; // ZABRANJENO - Credentials logirati LogDebug('Token: %s', [Token]); // ISPRAVNO - Environment Variables Token := GetEnvironmentVariable('GITHUB_TOKEN'); if Token = '' then raise EWvdSConfigError.Create(rsTokenNotConfigured); // ISPRAVNO - Maskirano logiranje LogDebug('Token configured: %s', [BoolToStr(Token <> '', True)]); ==== CWE-532: Log Injection ==== **Rizik:** Osjetljivi podaci vidljivi u logovima. // ZABRANJENO LogInfo('User login: %s with password: %s', [User, Password]); LogDebug('API response: %s', [FullResponse]); // Može sadržavati tokene! // ISPRAVNO LogInfo('User login: %s', [User]); // Bez lozinke LogDebug('API response received, length: %d', [Length(Response)]); ==== CWE-79: Cross-Site Scripting (XSS) ==== **Rizik:** Ubacivanje Script-koda u WebViews. // ZABRANJENO - Unescaped HTML WebView.Html := '
' + UserInput + '
'; // ISPRAVNO - Escaping function EscapeHtml(const AText: string): string; begin Result := AText; Result := StringReplace(Result, '&', '&', [rfReplaceAll]); Result := StringReplace(Result, '<', '<', [rfReplaceAll]); Result := StringReplace(Result, '>', '>', [rfReplaceAll]); Result := StringReplace(Result, '"', '"', [rfReplaceAll]); Result := StringReplace(Result, '''', ''', [rfReplaceAll]); end; WebView.Html := '
' + EscapeHtml(UserInput) + '
';
==== CWE-20: WebView Message Whitelist ==== **Rizik:** Nepoznati Message-tipovi omogućuju napade na WebView-handlere. Svaki WebView-Message-Handler MORA implementirati whitelistu dozvoljenih Message-tipova. // ZABRANJENO - Bez provjere Message-Type procedure HandleMessage(AMessage: TJSObject); var MsgType: string; begin MsgType := string(AMessage['type']); case MsgType of 'browse': HandleBrowse(AMessage); 'save': HandleSave(AMessage); end; end; // ISPRAVNO - S Whitelistom 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']); // Provjera Whiteliste PRVO if not IsAllowedMessageType(MsgType) then begin LogWarning(rsUnknownMessageType, [MsgType]); Exit; end; case MsgType of 'browse': HandleBrowse(AMessage); 'save': HandleSave(AMessage); // ... end; end; **Mjere:** * Definirati whitelistu sa svim dozvoljenim Message-tipovima * Logirati i odbiti nepoznate tipove * Ažurirati whitelistu kod novih funkcionalnosti ==== Defenzivni Node.js API-pozivi ==== **Rizik:** Node.js APIs mogu imati različite metode ovisno o kontekstu. Problem iz prakse: ''stdout.setEncoding'' nije funkcija ako Process nije ispravno inicijaliziran. // ZABRANJENO - Direktni pozivi metoda bez provjere procedure SetupProcessHandlers; begin asm this.FProcess.stdout.setEncoding('utf8'); this.FProcess.stderr.setEncoding('utf8'); end; end; // ISPRAVNO - Defenzivne typeof-provjere 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; **Mjere:** * ''typeof ... === 'function''' prije svakog poziva metode * Provjeriti postojanje objekta (''if (obj && obj.property)'') * Graceful obrada nedostajućih APIs ===== Error Handling ===== ==== Bez praznih Exception-Handlera ==== // ZABRANJENO try DoSomething; except // Gutanje greške end; // ISPRAVNO try DoSomething; except on E: ESpecificError do begin LogError(rsSpecificError, [E.Message]); // Obrada... end; on E: Exception do begin LogError(rsUnexpectedError, [E.ClassName, E.Message]); raise; // Ili odgovarajuće obraditi end; end; ==== Specifični Exception-tipovi ==== // ISPRAVNO - Specifične 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 zahtijeva **dva uvjeta**: - **Compile-Time:** ''{$IFDEF DEBUG}'' - **Runtime:** ''--debug'' Parametar {$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} ==== Što smije biti logirano ==== ^ Dozvoljeno ^ Zabranjeno ^ | Nazivi datoteka, putanje | Tokeni, API-Ključevi | | Nazivi akcija | Lozinke | | Numerički ID-evi | Session-ID-evi | | Poruke o greškama | Potpuni Requests/Responses | | Konfiguracijski ključevi | Konfiguracijske vrijednosti (osjetljive) | ===== Sigurne Default vrijednosti ===== // Sigurne standardne vrijednosti const DEFAULT_TIMEOUT = 30000; // 30 sekundi MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB MAX_RECURSION_DEPTH = 100; type TSpawnOptions = record Shell: Boolean; // Default: False (sigurno) Timeout: Integer; // Default: 30000 WorkingDir: string; Environment: TStringArray; end; function DefaultSpawnOptions: TSpawnOptions; begin Result.Shell := False; // VAŽNO: Bez Shell-a! Result.Timeout := DEFAULT_TIMEOUT; Result.WorkingDir := ''; Result.Environment := nil; end; ===== Code Review Checklista ===== Prije Mergea MORA se provjeriti: SIGURNOST: [ ] Nema hardkodiranih Credentials [ ] Svi korisnički unosi validirani [ ] Shell-Injection nije moguć [ ] Path-Traversal nije moguć [ ] HTML se escapea u WebViews [ ] Nema osjetljivih podataka u logovima ERROR HANDLING: [ ] Nema praznih Exception-handlera [ ] Specifične Exceptions obrađene [ ] Greške se logiraju (bez osjetljivih podataka) KONFIGURACIJA: [ ] Sigurne Default vrijednosti korištene [ ] Timeouts definirani [ ] Limiti definirani (veličina datoteke, rekurzija) ===== Sigurnosni Audit ==== Redovita provjera s automatiziranim alatima: ==== wvds-lint Sigurnosne provjere ==== wvds-lint security --path sources/ Provjerava se: * Exec/Shell-pozivi * Path-operacije bez validacije * Hardkodirani stringovi (potencijalni Credentials) * Prazni Exception-handleri * Log-pozivi s osjetljivim parametrima ==== Ručni Review ==== Za svaki novi Extension: * Provjeriti sve Entry-Points * Tracirati sve vanjske inpute * Provjeriti WebView-komunikaciju ===== Incident Response ===== Pri otkrivanju sigurnosne ranjivosti: - **Ne objavljivati** dok Patch nije spreman - Issue na GitHubu s labelom "security" (privatno) - Razviti i testirati Patch - Izdati novu verziju - Objaviti Advisory ===== Vidi također ===== * [[.:code-konventionen|Konvencije koda]] * [[.:debugging|Debugging]] * [[.:testing|Testing]] * [[https://owasp.org/www-project-top-ten/|OWASP Top 10]]