====== Varnostne smernice ====== Obvezujoči varnostni standardi za WvdS FPC RAD Suite, skladni s KRITIS/NIS2. Te smernice **niso pogajljive**. Koda, ki jih krši, ne bo sprejeta. ===== Preprečevanje OWASP Top 10 ===== ==== CWE-78: Vstavljanje ukazov ==== **Tveganje:** Izvajanje poljubnih ukazov prek manipuliranega vnosa. // PREPOVEDANO - Nevarno! Exec('cmd /c ' + UserInput); Shell('fpc ' + ProjectPath + ' ' + UserArgs); // PRAVILNO - Parametrizirano Options.Shell := False; Args := TStringArray.Create; Args.Add(ProjectPath); Spawn('fpc', Args, Options); **Ukrepi:** * Vedno uporabite ''shell: false'' * Argumente predajte kot polje * Validirajte poti pred uporabo ==== CWE-22: Prečkanje poti ==== **Tveganje:** Dostop do datotek izven dovoljenega imenika. // PREPOVEDANO - Nevarno! FileName := BasePath + UserInput; ReadFile(FileName); // PRAVILNO - Validacija function IsPathSafe(const ABasePath, AUserInput: string): Boolean; var ResolvedPath: string; begin // Brez .. dovoljenih 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)); **Ukrepi:** * Blokirajte sekvence ''..'' * Normalizirajte in validirajte poti * Osnovna pot mora biti predpona rezultatne poti ==== CWE-20: Validacija vnosa ==== **Tveganje:** Neveljavni podatki povzročijo napačno delovanje ali napad. // PREPOVEDANO - Brez validacije procedure CreateProject(const AName: string); begin MakeDir(AName); // Kaj če je AName = '../../../etc'? end; // PRAVILNO - 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: Poverilnice v čistem besedilu ==== **Tveganje:** Poverilnice v kodi ali dnevnikih. // PREPOVEDANO - Poverilnice v kodi const API_TOKEN = 'ghp_xxxxxxxxxxxx'; DB_PASSWORD = 'secret123'; // PREPOVEDANO - Beleženje poverilnic LogDebug('Token: %s', [Token]); // PRAVILNO - Okoljske spremenljivke Token := GetEnvironmentVariable('GITHUB_TOKEN'); if Token = '' then raise EWvdSConfigError.Create(rsTokenNotConfigured); // PRAVILNO - Maskirano beleženje LogDebug('Token configured: %s', [BoolToStr(Token <> '', True)]); ==== CWE-532: Vstavljanje v dnevnike ==== **Tveganje:** Občutljivi podatki vidni v dnevnikih. // PREPOVEDANO LogInfo('User login: %s with password: %s', [User, Password]); LogDebug('API response: %s', [FullResponse]); // Lahko vsebuje žetone! // PRAVILNO LogInfo('User login: %s', [User]); // Brez gesla LogDebug('API response received, length: %d', [Length(Response)]); ==== CWE-79: Medspletno izvajanje skript (XSS) ==== **Tveganje:** Vstavljanje skriptne kode v WebViews. // PREPOVEDANO - Neubežan HTML WebView.Html := '
' + UserInput + '
'; // PRAVILNO - Ubežanje 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: Beli seznam sporočil WebView ==== **Tveganje:** Neznani tipi sporočil omogočajo napade na obravnavalnike WebView. Vsak obravnavalnik sporočil WebView MORA implementirati beli seznam dovoljenih tipov sporočil. // PREPOVEDANO - Brez preverjanja tipa sporočila procedure HandleMessage(AMessage: TJSObject); var MsgType: string; begin MsgType := string(AMessage['type']); case MsgType of 'browse': HandleBrowse(AMessage); 'save': HandleSave(AMessage); end; end; // PRAVILNO - Z belim seznamom 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']); // Preverjanje belega seznama NAJPREJ if not IsAllowedMessageType(MsgType) then begin LogWarning(rsUnknownMessageType, [MsgType]); Exit; end; case MsgType of 'browse': HandleBrowse(AMessage); 'save': HandleSave(AMessage); // ... end; end; **Ukrepi:** * Definirajte beli seznam z vsemi dovoljenimi tipi sporočil * Zabeležite in zavrnite neznane tipe * Pri novih funkcijah posodobite beli seznam ==== Obrambni klici Node.js API ==== **Tveganje:** Node.js API-ji imajo lahko različne metode glede na kontekst. Problem iz prakse: ''stdout.setEncoding'' ni funkcija, če proces ni pravilno inicializiran. // PREPOVEDANO - Neposredni klici metod brez preverjanja procedure SetupProcessHandlers; begin asm this.FProcess.stdout.setEncoding('utf8'); this.FProcess.stderr.setEncoding('utf8'); end; end; // PRAVILNO - Obrambna preverjanja typeof 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; **Ukrepi:** * ''typeof ... === 'function''' pred vsakim klicem metode * Preverite obstoj objekta (''if (obj && obj.property)'') * Manjkajoče API-je obravnavajte elegantno ===== Obravnava napak ===== ==== Brez praznih obravnavalcev izjem ==== // PREPOVEDANO try DoSomething; except // Napaka se potlači end; // PRAVILNO try DoSomething; except on E: ESpecificError do begin LogError(rsSpecificError, [E.Message]); // Obravnava... end; on E: Exception do begin LogError(rsUnexpectedError, [E.ClassName, E.Message]); raise; // Ali ustrezno obravnavajte end; end; ==== Specifični tipi izjem ==== // PRAVILNO - Specifične izjeme 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; ===== Razhroščevalno beleženje ===== Razhroščevalno beleženje zahteva **dva pogoja**: - **Čas prevajanja:** ''{$IFDEF DEBUG}'' - **Čas izvajanja:** parameter ''--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} ==== Kaj se sme beležiti ==== ^ Dovoljeno ^ Prepovedano ^ | Imena datotek, poti | Žetoni, API ključi | | Imena akcij | Gesla | | Numerični ID-ji | ID-ji sej | | Sporočila napak | Popolni zahtevki/odgovori | | Konfiguracijski ključi | Konfiguracijske vrednosti (občutljive) | ===== Varne privzete vrednosti ===== // Varne privzete vrednosti const DEFAULT_TIMEOUT = 30000; // 30 sekund MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB MAX_RECURSION_DEPTH = 100; type TSpawnOptions = record Shell: Boolean; // Privzeto: False (varno) Timeout: Integer; // Privzeto: 30000 WorkingDir: string; Environment: TStringArray; end; function DefaultSpawnOptions: TSpawnOptions; begin Result.Shell := False; // POMEMBNO: Brez lupine! Result.Timeout := DEFAULT_TIMEOUT; Result.WorkingDir := ''; Result.Environment := nil; end; ===== Kontrolni seznam pregledov kode ===== Pred združitvijo JE TREBA preveriti: VARNOST: [ ] Brez zakodiranih poverilnic [ ] Vsi uporabniški vnosi so validirani [ ] Vstavljanje ukazov ni mogoče [ ] Prečkanje poti ni mogoče [ ] HTML je ubežan v WebViews [ ] Brez občutljivih podatkov v dnevnikih OBRAVNAVA NAPAK: [ ] Brez praznih obravnavalcev izjem [ ] Specifične izjeme obravnavane [ ] Napake se beležijo (brez občutljivih podatkov) KONFIGURACIJA: [ ] Uporabljene varne privzete vrednosti [ ] Časovne omejitve definirane [ ] Omejitve definirane (velikost datoteke, rekurzija) ===== Varnostna revizija ==== Redna preverjanja z avtomatiziranimi orodji: ==== Varnostna preverjanja wvds-lint ==== wvds-lint security --path sources/ Preveri se: * Klici Exec/Shell * Operacije poti brez validacije * Zakodirani nizi (potencialne poverilnice) * Prazni obravnavalci izjem * Klici beleženja z občutljivimi parametri ==== Ročni pregled ==== Pri vsaki novi razširitvi: * Preverite vse vstopne točke * Sledite vsem zunanjim vnosom * Preverite komunikacijo WebView ===== Odziv na incidente ===== Ob odkritju varnostne ranljivosti: - **Ne objavite** dokler popravek ni pripravljen - Ustvarite issue na GitHub z oznako "security" (zasebno) - Razvijte in testirajte popravek - Objavite novo različico - Objavite obvestilo ===== Glejte tudi ===== * [[.:code-konventionen|Kodne konvencije]] * [[.:debugging|Razhroščevanje]] * [[.:testing|Testiranje]] * [[https://owasp.org/www-project-top-ten/|OWASP Top 10]]