Mandatory security standards for WvdS FPC RAD Suite, compliant with KRITIS/NIS2.
Risk: Execution of arbitrary commands through manipulated input.
// FORBIDDEN - Insecure! Exec('cmd /c ' + UserInput); Shell('fpc ' + ProjectPath + ' ' + UserArgs); // CORRECT - Parameterized Options.Shell := False; Args := TStringArray.Create; Args.Add(ProjectPath); Spawn('fpc', Args, Options);
Measures:
shell: falseRisk: Access to files outside the allowed directory.
// FORBIDDEN - Insecure! FileName := BasePath + UserInput; ReadFile(FileName); // CORRECT - Validation function IsPathSafe(const ABasePath, AUserInput: string): Boolean; var ResolvedPath: string; begin // Do not allow .. 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));
Measures:
.. sequencesRisk: Invalid data leads to malfunction or attack.
// FORBIDDEN - No validation procedure CreateProject(const AName: string); begin MakeDir(AName); // What if AName = '../../../etc'? end; // CORRECT - Validation 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;
Risk: Credentials in code or logs.
// FORBIDDEN - Credentials in code const API_TOKEN = 'ghp_xxxxxxxxxxxx'; DB_PASSWORD = 'secret123'; // FORBIDDEN - Logging credentials LogDebug('Token: %s', [Token]); // CORRECT - Environment Variables Token := GetEnvironmentVariable('GITHUB_TOKEN'); if Token = '' then raise EWvdSConfigError.Create(rsTokenNotConfigured); // CORRECT - Masked logging LogDebug('Token configured: %s', [BoolToStr(Token <> '', True)]);
Risk: Sensitive data visible in logs.
// FORBIDDEN LogInfo('User login: %s with password: %s', [User, Password]); LogDebug('API response: %s', [FullResponse]); // May contain tokens! // CORRECT LogInfo('User login: %s', [User]); // No password LogDebug('API response received, length: %d', [Length(Response)]);
Risk: Injection of script code into WebViews.
// FORBIDDEN - Unescaped HTML WebView.Html := '<div>' + UserInput + '</div>'; // CORRECT - 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 := '<div>' + EscapeHtml(UserInput) + '</div>';
Risk: Unknown message types enable attacks on WebView handlers.
// FORBIDDEN - No 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; // CORRECT - With 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 check FIRST if not IsAllowedMessageType(MsgType) then begin LogWarning(rsUnknownMessageType, [MsgType]); Exit; end; case MsgType of 'browse': HandleBrowse(AMessage); 'save': HandleSave(AMessage); // ... end; end;
Measures:
Risk: Node.js APIs can have different methods depending on context.
stdout.setEncoding is not a function when process is not correctly initialized.
// FORBIDDEN - Direct method calls without check procedure SetupProcessHandlers; begin asm this.FProcess.stdout.setEncoding('utf8'); this.FProcess.stderr.setEncoding('utf8'); end; end; // CORRECT - 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;
Measures:
typeof … === 'function' before every method callif (obj && obj.property))// FORBIDDEN try DoSomething; except // Swallowing errors end; // CORRECT try DoSomething; except on E: ESpecificError do begin LogError(rsSpecificError, [E.Message]); // Handle... end; on E: Exception do begin LogError(rsUnexpectedError, [E.ClassName, E.Message]); raise; // Or handle appropriately end; end;
// CORRECT - Specific 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 requires two conditions:
{$IFDEF DEBUG}–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}
| Allowed | Forbidden |
|---|---|
| Filenames, paths | Tokens, API keys |
| Action names | Passwords |
| Numeric IDs | Session IDs |
| Error messages | Full requests/responses |
| Configuration keys | Configuration values (sensitive) |
// Secure default values const DEFAULT_TIMEOUT = 30000; // 30 seconds MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB MAX_RECURSION_DEPTH = 100; type TSpawnOptions = record Shell: Boolean; // Default: False (secure) Timeout: Integer; // Default: 30000 WorkingDir: string; Environment: TStringArray; end; function DefaultSpawnOptions: TSpawnOptions; begin Result.Shell := False; // IMPORTANT: No shell! Result.Timeout := DEFAULT_TIMEOUT; Result.WorkingDir := ''; Result.Environment := nil; end;
Before merge MUST be checked:
SECURITY: [ ] No hardcoded credentials [ ] All user inputs validated [ ] No shell injection possible [ ] No path traversal possible [ ] HTML is escaped in WebViews [ ] No sensitive data in logs ERROR HANDLING: [ ] No empty exception handlers [ ] Specific exceptions handled [ ] Errors are logged (without sensitive data) CONFIGURATION: [ ] Secure defaults used [ ] Timeouts defined [ ] Limits defined (file size, recursion)
Regular checking with automated tools:
wvds-lint security --path sources/
Checks:
For every new extension:
When a security vulnerability is discovered: