Browse Source

Merge branch 'main' into FormBackgroundStyleHook

Updated whatsnew during merge.

# Conflicts:
#	whatsnew.htm
Martijn Laan 2 months ago
parent
commit
a1121b0ca0

+ 40 - 0
ISHelp/isetup.xml

@@ -1096,6 +1096,7 @@ DefaultGroupName=My Program
 <li><link topic="setup_password">Password</link></li>
 <li><link topic="setup_privilegesrequired">PrivilegesRequired</link></li>
 <li><link topic="setup_privilegesrequiredoverridesallowed">PrivilegesRequiredOverridesAllowed</link></li>
+<li><link topic="setup_redirectionguard">RedirectionGuard</link></li>
 <li><link topic="setup_restartapplications">RestartApplications</link></li>
 <li><link topic="setup_restartifneededbyrun">RestartIfNeededByRun</link></li>
 <li><link topic="setup_setuplogging">SetupLogging</link></li>
@@ -3950,6 +3951,8 @@ issigtool --key-file="MyKey.ispublickey" verify "MyProg.dll"
 <keyword value="/LOGCLOSEAPPLICATIONS" anchor="LOGCLOSEAPPLICATIONS" />
 <keyword value="/RESTARTAPPLICATIONS" anchor="RESTARTAPPLICATIONS" />
 <keyword value="/NORESTARTAPPLICATIONS" anchor="NORESTARTAPPLICATIONS" />
+<keyword value="/REDIRECTIONGUARD" anchor="REDIRECTIONGUARD" />
+<keyword value="/NOREDIRECTIONGUARD" anchor="NOREDIRECTIONGUARD" />
 <keyword value="/LOADINF=" anchor="LOADINF" />
 <keyword value="/SAVEINF=" anchor="SAVEINF" />
 <keyword value="/LANG=" anchor="LANG" />
@@ -4095,6 +4098,16 @@ issigtool --key-file="MyKey.ispublickey" verify "MyProg.dll"
 <p>Prevents Setup from restarting applications. If /RESTARTAPPLICATIONS was also used, this command line parameter is ignored.</p>
 </dd>
 
+<dt><b><a name="REDIRECTIONGUARD">/REDIRECTIONGUARD</a></b></dt>
+<dd>
+<p>Instructs Setup to enable RedirectionGuard if possible.</p>
+</dd>
+
+<dt><b><a name="NOREDIRECTIONGUARD">/NOREDIRECTIONGUARD</a></b></dt>
+<dd>
+<p>Prevents Setup from enabling RedirectionGuard. If /REDIRECTIONGUARD was also used, this command line parameter is ignored.</p>
+</dd>
+
 <dt><b><a name="LOADINF">/LOADINF=</a>"<i>filename</i>"</b></dt>
 <dd>
 <p>Instructs Setup to load the settings from the specified file after having checked the command line. This file can be prepared using the '/SAVEINF=' command as explained below.</p>
@@ -4284,6 +4297,8 @@ Keep the default set of selected tasks, but deselect the "desktopicon" task:<br/
 <keyword value="/VERYSILENT" anchor="VERYSILENT" />
 <keyword value="/NORESTART" anchor="NORESTART" />
 <keyword value="/NOSTYLE" anchor="NOSTYLE" />
+<keyword value="/REDIRECTIONGUARD" anchor="REDIRECTIONGUARD" />
+<keyword value="/NOREDIRECTIONGUARD" anchor="NOREDIRECTIONGUARD" />
 <keyword value="/LOG" anchor="LOG" />
 <keyword value="/LOG=" anchor="LOG2" />
 <keyword value="/SUPPRESSMSGBOXES" anchor="SUPPRESSMSGBOXES" />
@@ -4330,6 +4345,16 @@ Keep the default set of selected tasks, but deselect the "desktopicon" task:<br/
 <p>Instructs the uninstaller not to reboot even if it's necessary.</p>
 </dd>
 
+<dt><b><a name="REDIRECTIONGUARD">/REDIRECTIONGUARD</a></b></dt>
+<dd>
+<p>Instructs the uninstaller to enable RedirectionGuard if possible.</p>
+</dd>
+
+<dt><b><a name="NOREDIRECTIONGUARD">/NOREDIRECTIONGUARD</a></b></dt>
+<dd>
+<p>Prevents the uninstaller from enabling RedirectionGuard. If /REDIRECTIONGUARD was also used, this command line parameter is ignored.</p>
+</dd>
+
 </dl>
 
 </body>
@@ -4841,6 +4866,21 @@ Name: portablemode; Description: "Portable Mode"</pre></example>
 </body>
 </setuptopic>
 
+<setuptopic directive="RedirectionGuard">
+<setupvalid><link topic="yesnonotes"><tt>yes</tt> or <tt>no</tt></link></setupvalid>
+<setupdefault><tt>yes</tt></setupdefault>
+<body>
+<p>This directive lets you disable Setup and Uninstall always trying to enable Windows RedirectionGuard.</p>
+<p>This mitigation blocks traversal of NTFS junctions created by non-admin users, to prevent path redirection vulnerabilities when privileged installers touch attacker-controlled directories.</p>
+<p>Can also be disabled or enabled using Setup and Uninstall command line parameters /NOREDIRECTIONGUARD and /REDIRECTIONGUARD.</p>
+<p>When no command line parameter is specified, Uninstall enables or disables RedirectionGuard based on the latest install.</p>
+<p>RedirectionGuard status is logged, so you can verify whether the mitigation was active.</p>
+<p><b>See also:</b><br/>
+<link topic="setupcmdline">Setup Command Line Parameters</link><br/>
+<link topic="uninstcmdline">Uninstaller Command Line Parameters</link></p>
+</body>
+</setuptopic>
+
 <setuptopic directive="DisablePrecompiledFileVerifications">
 <setupvalid>One or more of the following, separated by spaces:<br/><tt>setupe32</tt><br/><tt>setupcustomstylee32</tt><br/><tt>setupldre32</tt><br/><tt>is7zdll</tt><br/><tt>isbunzipdll</tt><br/><tt>isunzlibdll</tt><br/><tt>islzmaexe</tt></setupvalid>
 <setupdefault><i>(blank)</i></setupdefault>

+ 14 - 4
Projects/SetupLdr.dpr

@@ -186,10 +186,15 @@ var
 begin
   CmdLine := '"' + Filename + '" ' + Parms;
 
+  { Pass current directory in "final" reparsed form so that Setup won't have
+    trouble accessing the directory after enabling RedirectionGuard if there's
+    an untrusted redirect in the path. }
+  const WorkingDir = GetFinalCurrentDir;
+
   FillChar(StartupInfo, SizeOf(StartupInfo), 0);
   StartupInfo.cb := SizeOf(StartupInfo);
-  if not CreateProcess(nil, PChar(CmdLine), nil, nil, False, 0, nil, nil,
-     StartupInfo, ProcessInfo) then
+  if not CreateProcess(nil, PChar(CmdLine), nil, nil, False, 0, nil,
+     PChar(WorkingDir), StartupInfo, ProcessInfo) then
     RaiseLastError(msgLdrCannotExecTemp);
   CloseHandle(ProcessInfo.hThread);
   { Wait for the process to terminate, processing messages in the meantime }
@@ -347,6 +352,8 @@ begin
           'Instructs Setup to create extra logging when closing applications for debugging purposes.' + SNewLine +
           '/RESTARTAPPLICATIONS, /NORESTARTAPPLICATIONS' + SNewLine +
           'Instructs Setup to attempt restarting applications, or prevents it from doing so.' + SNewLine +
+          '/REDIRECTIONGUARD, /NOREDIRECTIONGUARD' + SNewLine +
+          'Instructs Setup to attempt enabling RedirectionGuard, or prevents it from doing so.' + SNewLine +
           '/LOADINF="filename", /SAVEINF="filename"' + SNewLine +
           'Instructs Setup to load the settings from the specified file after having checked the command line, or to save them to it.' + SNewLine +
           '/LANG=language' + SNewLine +
@@ -523,10 +530,13 @@ begin
         Longint(OrigWndProc) := SetWindowLong(SetupLdrWnd, GWL_WNDPROC,
           Longint(@SetupLdrWndProc));
 
-        { Now execute Setup. Use the exit code it returns as our exit code. }
+        { Now execute Setup. Use the exit code it returns as our exit code.
+          SelfFilename is passed in "final" reparsed form so that Setup won't
+          have trouble accessing the file after enabling RedirectionGuard if
+          there's an untrusted redirect in the path. }
         ExecAndWait(TempFile, Format('/SL5="$%x,%d,%d,',
           [UInt32(SetupLdrWnd), OffsetTable.Offset0, OffsetTable.Offset1]) +
-          SelfFilename + '" ' + GetCmdTail, SetupLdrExitCode);
+          GetFinalFileName(SelfFilename) + '" ' + GetCmdTail, SetupLdrExitCode);
 
         { Synchronize our active language with Setup's, in case we need to
           display any messages below } 

+ 4 - 1
Projects/Src/Compiler.SetupCompiler.pas

@@ -3087,6 +3087,9 @@ begin
     ssPrivilegesRequiredOverridesAllowed: begin
         SetupHeader.PrivilegesRequiredOverridesAllowed := StrToPrivilegesRequiredOverrides(Value);
       end;
+    ssRedirectionGuard: begin
+        SetSetupHeaderOption(shRedirectionGuard);
+      end;
     ssReserveBytes: begin
         Val(Value, ReserveBytes, I);
         if (I <> 0) or (ReserveBytes < 0) then
@@ -7981,7 +7984,7 @@ begin
       shAllowCancelDuringInstall, shWizardImageStretch, shAppendDefaultDirName,
       shAppendDefaultGroupName, shUsePreviousLanguage, shCloseApplications,
       shRestartApplications, shAllowNetworkDrive, shDisableWelcomePage,
-      shUsePreviousPrivileges, shWizardKeepAspectRatio];
+      shUsePreviousPrivileges, shWizardKeepAspectRatio, shRedirectionGuard];
     SetupHeader.PrivilegesRequired := prAdmin;
     SetupHeader.UninstallFilesDir := '{app}';
     SetupHeader.DefaultUserInfoName := '{sysuserinfoname}';

+ 0 - 7
Projects/Src/Setup.InstFunc.pas

@@ -82,7 +82,6 @@ function IsProtectedSystemFile(const DisableFsRedir: Boolean;
   const Filename: String): Boolean;
 function MakePendingFileRenameOperationsChecksum: TSHA256Digest;
 function ModifyPifFile(const Filename: String; const CloseOnExit: Boolean): Boolean;
-procedure RaiseFunctionFailedError(const FunctionName: String);
 procedure RaiseOleError(const FunctionName: String; const ResultCode: HRESULT);
 procedure RefreshEnvironment;
 function ReplaceSystemDirWithSysWow64(const Path: String): String;
@@ -129,12 +128,6 @@ begin
     [FunctionName, IntToHexStr8(ResultCode), Win32ErrorString(ResultCode)]));
 end;
 
-procedure RaiseFunctionFailedError(const FunctionName: String);
-begin
-  raise Exception.Create(FmtSetupMessage1(msgErrorFunctionFailedNoCode,
-    FunctionName));
-end;
-
 function GetRegRootKeyName(const RootKey: HKEY): String;
 begin
   case RootKey of

+ 2 - 0
Projects/Src/Setup.Install.pas

@@ -2822,6 +2822,8 @@ begin
         Include(UninstLog.Flags, ufAlwaysRestart);
       if ChangesEnvironment then
         Include(UninstLog.Flags, ufChangesEnvironment);
+      if RedirectionGuardEnabled then
+        Include(UninstLog.Flags, ufRedirectionGuard);
       UninstLog.WizardSizePercentX := SetupHeader.WizardSizePercentX;
       UninstLog.WizardSizePercentY := SetupHeader.WizardSizePercentY;
       UninstLog.WizardBackColor := OrigSetupHeaderWizardBackColor; { See Setup.MainFunc }

+ 51 - 1
Projects/Src/Setup.MainFunc.pas

@@ -79,7 +79,7 @@ var
   InitNoIcons, InitSilent, InitVerySilent, InitNoRestart, InitCloseApplications,
     InitNoCloseApplications, InitForceCloseApplications, InitNoForceCloseApplications,
     InitLogCloseApplications, InitRestartApplications, InitNoRestartApplications,
-    InitNoCancel, InitNoStyle: Boolean;
+    InitNoCancel, InitNoStyle, InitRedirectionGuard, InitNoRedirectionGuard: Boolean;
   InitSetupType: String;
   InitComponents, InitTasks: TStringList;
   InitComponentsSpecified: Boolean;
@@ -213,6 +213,8 @@ procedure NotifyAfterInstallEntry(const AfterInstall: String);
 procedure NotifyAfterInstallFileEntry(const FileEntry: PSetupFileEntry);
 procedure NotifyBeforeInstallEntry(const BeforeInstall: String);
 procedure NotifyBeforeInstallFileEntry(const FileEntry: PSetupFileEntry);
+procedure RedirectionGuardConfigure(const AEnable: Boolean);
+function RedirectionGuardEnabled: Boolean;
 function PreviousInstallCompleted(const WizardComponents, WizardTasks: TStringList): Boolean;
 function CodeRegisterExtraCloseApplicationsResource(const DisableFsRedir: Boolean; const AFilename: String): Boolean;
 procedure RegisterResourcesWithRestartManager(const WizardComponents, WizardTasks: TStringList);
@@ -704,6 +706,8 @@ begin
   InitNoRestartApplications := GetIniBool(Section, 'NoRestartApplications', InitNoRestartApplications, FileName);
   InitNoCancel := GetIniBool(Section, 'NoCancel', InitNoCancel, FileName);
   InitNoStyle := GetIniBool(Section, 'NoStyle', InitNoStyle, FileName);
+  InitRedirectionGuard := GetIniBool(Section, 'RedirectionGuard', InitRedirectionGuard, FileName);
+  InitNoRedirectionGuard := GetIniBool(Section, 'NoRedirectionGuard', InitNoRedirectionGuard, FileName);
   InitPassword := GetIniString(Section, 'Password', InitPassword, FileName);
   InitRestartExitCode := GetIniInt(Section, 'RestartExitCode', InitRestartExitCode, 0, 0, FileName);
   WantToSuppressMsgBoxes := GetIniBool(Section, 'SuppressMsgBoxes', WantToSuppressMsgBoxes, FileName);
@@ -2469,6 +2473,44 @@ begin
   SetActiveLanguage(I);
 end;
 
+var
+  IsRedirectionGuardEnabled: Boolean;
+
+procedure RedirectionGuardConfigure(const AEnable: Boolean);
+const
+  ProcessRedirectionTrustPolicy = TProcessMitigationPolicy(16);
+var
+  SetProcessMitigationPolicyFunc: function(MitigationPolicy: TProcessMitigationPolicy;
+    lpBuffer: PVOID; dwLength: SIZE_T): BOOL; stdcall;
+begin
+  var Status: String;
+
+  if AEnable then begin
+    SetProcessMitigationPolicyFunc := GetProcAddress(GetModuleHandle(kernel32),
+      PAnsiChar('SetProcessMitigationPolicy'));
+    if Assigned(SetProcessMitigationPolicyFunc) then begin
+      const Flags: DWORD = 1;  { = EnforceRedirectionTrust bit set }
+      if SetProcessMitigationPolicyFunc(ProcessRedirectionTrustPolicy, @Flags, SizeOf(Flags)) then begin
+        IsRedirectionGuardEnabled := True;
+        Status := 'Enabled in enforcing mode'
+      end else begin
+        const ErrorCode = GetLastError;
+        Status := Format('Could not enable (SetProcessMitigationPolicy failed with error code %u)',
+          [ErrorCode]);
+      end;
+    end else
+      Status := 'Could not enable (SetProcessMitigationPolicy unavailable)';
+  end else
+    Status := 'Not enabling';
+
+  LogFmt('RedirectionGuard status for current process: %s', [Status]);
+end;
+
+function RedirectionGuardEnabled: Boolean;
+begin
+  Result := IsRedirectionGuardEnabled;
+end;
+
 procedure LogCompatibilityMode;
 var
   S: String;
@@ -3198,6 +3240,10 @@ begin
       InitRestartApplications := True
     else if SameText(ParamName, '/NoRestartApplications') then
       InitNoRestartApplications := True
+    else if SameText(ParamName, '/RedirectionGuard') then
+      InitRedirectionGuard := True
+    else if SameText(ParamName, '/NoRedirectionGuard') then
+      InitNoRedirectionGuard := True
     else if SameText(ParamName, '/NoIcons') then
       InitNoIcons := True
     else if SameText(ParamName, '/NoCancel') then
@@ -3547,6 +3593,10 @@ begin
 
   Log64BitInstallMode;
 
+  const EnableRedirectionGuard = InitRedirectionGuard or
+    ((shRedirectionGuard in SetupHeader.Options) and not InitNoRedirectionGuard);
+  RedirectionGuardConfigure(EnableRedirectionGuard);
+
   { Test code. Originally planned to call DeleteResidualTempUninstallDirs
     during Setup's startup too, but decided against it; it's not really
     necessary and could slow down the startup (slightly). }

+ 2 - 72
Projects/Src/Setup.SpawnServer.pas

@@ -46,7 +46,8 @@ implementation
 {x$DEFINE SPAWNSERVER_RESPAWN_ALWAYS}
 
 uses
-  Classes, Forms, ShellApi, PathFunc, Shared.CommonFunc, Setup.InstFunc, Setup.SpawnCommon;
+  Classes, Forms, ShellApi, PathFunc, Shared.CommonFunc,
+  SetupLdrAndSetup.InstFunc, Setup.InstFunc, Setup.SpawnCommon;
 
 type
   TPtrAndSize = record
@@ -155,77 +156,6 @@ begin
 end;
 {$ENDIF}
 
-function GetFinalFileName(const Filename: String): String;
-{ Calls GetFinalPathNameByHandle to expand any SUBST'ed drives, network drives,
-  and symbolic links in Filename. This is needed for elevation to succeed when
-  Setup is started from a SUBST'ed drive letter. }
-
-  function ConvertToNormalPath(P: PChar): String;
-  begin
-    Result := P;
-    if StrLComp(P, '\\?\', 4) = 0 then begin
-      Inc(P, 4);
-      if (PathStrNextChar(P) = P + 1) and (P[1] = ':') and PathCharIsSlash(P[2]) then
-        Result := P
-      else if StrLIComp(P, 'UNC\', 4) = 0 then begin
-        Inc(P, 4);
-        Result := '\\' + P;
-      end;
-    end;
-  end;
-
-const
-  FILE_SHARE_DELETE = $00000004;
-var
-  GetFinalPathNameByHandleFunc: function(hFile: THandle; lpszFilePath: PWideChar;
-    cchFilePath: DWORD; dwFlags: DWORD): DWORD; stdcall;
-  Attr, FlagsAndAttributes: DWORD;
-  H: THandle;
-  Res: Integer;
-  Buf: array[0..4095] of Char;
-begin
-  GetFinalPathNameByHandleFunc := GetProcAddress(GetModuleHandle(kernel32),
-    'GetFinalPathNameByHandleW');
-  if Assigned(GetFinalPathNameByHandleFunc) then begin
-    Attr := GetFileAttributes(PChar(Filename));
-    if Attr <> INVALID_FILE_ATTRIBUTES then begin
-      { Backup semantics must be requested in order to open a directory }
-      if Attr and FILE_ATTRIBUTE_DIRECTORY <> 0 then
-        FlagsAndAttributes := FILE_FLAG_BACKUP_SEMANTICS
-      else
-        FlagsAndAttributes := 0;
-      { Use zero access mask and liberal sharing mode to ensure success }
-      H := CreateFile(PChar(Filename), 0, FILE_SHARE_READ or FILE_SHARE_WRITE or
-        FILE_SHARE_DELETE, nil, OPEN_EXISTING, FlagsAndAttributes, 0);
-      if H <> INVALID_HANDLE_VALUE then begin
-        Res := GetFinalPathNameByHandleFunc(H, Buf, SizeOf(Buf) div SizeOf(Buf[0]), 0);
-        CloseHandle(H);
-        if (Res > 0) and (Res < (SizeOf(Buf) div SizeOf(Buf[0])) - 16) then begin
-          { ShellExecuteEx fails with error 3 on \\?\UNC\ paths, so try to
-            convert the returned path from \\?\ form }
-          Result := ConvertToNormalPath(Buf);
-          Exit;
-        end;
-      end;
-    end;
-  end;
-  Result := Filename;
-end;
-
-function GetFinalCurrentDir: String;
-var
-  Res: Integer;
-  Buf: array[0..MAX_PATH-1] of Char;
-begin
-  DWORD(Res) := GetCurrentDirectory(SizeOf(Buf) div SizeOf(Buf[0]), Buf);
-  if (Res > 0) and (Res < SizeOf(Buf) div SizeOf(Buf[0])) then
-    Result := GetFinalFileName(Buf)
-  else begin
-    RaiseFunctionFailedError('GetCurrentDirectory');
-    Result := '';
-  end;
-end;
-
 procedure RespawnSelfElevated(const AExeFilename, AParams: String;
   var AExitCode: DWORD);
 { Spawns a new process using the "runas" verb.

+ 8 - 0
Projects/Src/Setup.Uninstall.pas

@@ -313,6 +313,10 @@ begin
       NoRestart := True
     else if SameText(ParamName, '/NoStyle') then
       InitNoStyle := True
+    else if SameText(ParamName, '/RedirectionGuard') then
+      InitRedirectionGuard := True
+    else if SameText(ParamName, '/NoRedirectionGuard') then
+      InitNoRedirectionGuard := True
     else if SameText(ParamName, '/SuppressMsgBoxes') then
       WantToSuppressMsgBoxes := True
     else if SameText(ParamName, '/DEBUGWND=') then begin
@@ -634,6 +638,10 @@ begin
     else
       Initialize64BitInstallMode(False);
 
+    const EnableRedirectionGuard = InitRedirectionGuard or
+      ((ufRedirectionGuard in UninstLog.Flags) and not InitNoRedirectionGuard);
+    RedirectionGuardConfigure(EnableRedirectionGuard);
+
     DeleteResidualTempUninstallDirs;
 
     { Create temporary directory and extract 64-bit helper EXE if necessary }

+ 9 - 3
Projects/Src/Setup.UninstallLog.pas

@@ -29,6 +29,9 @@ const
     are in sync again. While technically not required, this approach keeps things
     more sensible.
 
+    Note that something as simple as adding a new [Code] function is an improvement
+    to Uninstall.
+
     If you want to customize the uninstall log but maintain compatibility with
     official Inno Setup releases, you should NOT do any of the above. Instead, it's
     recommended to use the "utUserDefined" log entry type if you wish to implement
@@ -116,7 +119,8 @@ type
     ufWizardModern, ufAlwaysRestart, ufChangesEnvironment, ufWin64,
     ufPowerUserInstalled, ufAdminInstallMode, ufWizardDarkStyleDark,
     ufWizardDarkStyleDynamic, ufWizardBorderStyled,
-    ufWizardLightButtonsUnstyled, ufWizardKeepAspectRatio);
+    ufWizardLightButtonsUnstyled, ufWizardKeepAspectRatio,
+    ufRedirectionGuard);
 
   TUninstallLog = class
   private
@@ -176,7 +180,9 @@ uses
 
 type
   { Note: TUninstallLogHeader should stay <= 512 bytes in size, so that it
-    fits into a single disk sector and can be written atomically }
+    fits into a single disk sector and can be written atomically.
+    Do not add "non-sticky" flags and fields that are set only by the
+    latest installer. Add these to TMessagesLangOptions instead. }
   TUninstallLogHeader = packed record
     ID: TUninstallLogID;
     AppId: array[0..127] of AnsiChar;
@@ -1236,7 +1242,7 @@ var
 
   function GetNonStickyFlags: TUninstallLogFlags;
   begin
-    Result := GetWizardFlags;
+    Result := GetWizardFlags + [ufRedirectionGuard];
   end;
 
 var

+ 79 - 0
Projects/Src/SetupLdrAndSetup.InstFunc.pas

@@ -27,6 +27,9 @@ procedure DelayDeleteFile({$IFDEF SETUPPROJ}const DisableFsRedir: Boolean;{$ENDI
 function DetermineDefaultLanguage(const GetLanguageEntryProc: TGetLanguageEntryProc;
   const Method: TSetupLanguageDetectionMethod; const LangParameter: String;
   var ResultIndex: Integer): TDetermineDefaultLanguageResult;
+function GetFinalCurrentDir: String;
+function GetFinalFileName(const Filename: String): String;
+procedure RaiseFunctionFailedError(const FunctionName: String);
 function RestartComputer: Boolean;
 procedure SplitNewParamStr(const Index: Integer; var AName, AValue: String);
 
@@ -366,4 +369,80 @@ begin
   AValue := '';
 end;
 
+procedure RaiseFunctionFailedError(const FunctionName: String);
+begin
+  raise Exception.Create(FmtSetupMessage1(msgErrorFunctionFailedNoCode,
+    FunctionName));
+end;
+
+function GetFinalFileName(const Filename: String): String;
+{ Calls GetFinalPathNameByHandle to expand any SUBST'ed drives, network drives,
+  and symbolic links in Filename. This is needed for elevation to succeed when
+  Setup is started from a SUBST'ed drive letter. }
+
+  function ConvertToNormalPath(P: PChar): String;
+  begin
+    Result := P;
+    if StrLComp(P, '\\?\', 4) = 0 then begin
+      Inc(P, 4);
+      if (PathStrNextChar(P) = P + 1) and (P[1] = ':') and PathCharIsSlash(P[2]) then
+        Result := P
+      else if StrLIComp(P, 'UNC\', 4) = 0 then begin
+        Inc(P, 4);
+        Result := '\\' + P;
+      end;
+    end;
+  end;
+
+const
+  FILE_SHARE_DELETE = $00000004;
+var
+  GetFinalPathNameByHandleFunc: function(hFile: THandle; lpszFilePath: PWideChar;
+    cchFilePath: DWORD; dwFlags: DWORD): DWORD; stdcall;
+  Attr, FlagsAndAttributes: DWORD;
+  H: THandle;
+  Buf: array[0..4095] of Char;
+begin
+  GetFinalPathNameByHandleFunc := GetProcAddress(GetModuleHandle(kernel32),
+    'GetFinalPathNameByHandleW');
+  if Assigned(GetFinalPathNameByHandleFunc) then begin
+    Attr := GetFileAttributes(PChar(Filename));
+    if Attr <> INVALID_FILE_ATTRIBUTES then begin
+      { Backup semantics must be requested in order to open a directory }
+      if Attr and FILE_ATTRIBUTE_DIRECTORY <> 0 then
+        FlagsAndAttributes := FILE_FLAG_BACKUP_SEMANTICS
+      else
+        FlagsAndAttributes := 0;
+      { Use zero access mask and liberal sharing mode to ensure success }
+      H := CreateFile(PChar(Filename), 0, FILE_SHARE_READ or FILE_SHARE_WRITE or
+        FILE_SHARE_DELETE, nil, OPEN_EXISTING, FlagsAndAttributes, 0);
+      if H <> INVALID_HANDLE_VALUE then begin
+        const Res = GetFinalPathNameByHandleFunc(H, Buf, SizeOf(Buf) div SizeOf(Buf[0]), 0);
+        CloseHandle(H);
+        if (Res > 0) and (Res < (SizeOf(Buf) div SizeOf(Buf[0])) - 16) then begin
+          { ShellExecuteEx fails with error 3 on \\?\UNC\ paths, so try to
+            convert the returned path from \\?\ form }
+          Result := ConvertToNormalPath(Buf);
+          Exit;
+        end;
+      end;
+    end;
+  end;
+  Result := Filename;
+end;
+
+function GetFinalCurrentDir: String;
+var
+  Res: Integer;
+  Buf: array[0..MAX_PATH-1] of Char;
+begin
+  DWORD(Res) := GetCurrentDirectory(SizeOf(Buf) div SizeOf(Buf[0]), Buf);
+  if (Res > 0) and (Res < SizeOf(Buf) div SizeOf(Buf[0])) then
+    Result := GetFinalFileName(Buf)
+  else begin
+    RaiseFunctionFailedError('GetCurrentDirectory');
+    Result := '';
+  end;
+end;
+
 end.

+ 1 - 0
Projects/Src/Shared.SetupSectionDirectives.pas

@@ -110,6 +110,7 @@ type
     ssPassword,
     ssPrivilegesRequired,
     ssPrivilegesRequiredOverridesAllowed,
+    ssRedirectionGuard,
     ssReserveBytes,
     ssRestartApplications,
     ssRestartIfNeededByRun,

+ 9 - 3
Projects/Src/Shared.Struct.pas

@@ -65,7 +65,8 @@ type
     shCloseApplications, shRestartApplications, shAllowNetworkDrive,
     shForceCloseApplications, shAppNameHasConsts, shUsePreviousPrivileges,
     shUninstallLogging, shWizardModern, shWizardBorderStyled,
-    shWizardKeepAspectRatio, shWizardLightButtonsUnstyled);
+    shWizardKeepAspectRatio, shWizardLightButtonsUnstyled,
+    shRedirectionGuard);
   TSetupLanguageDetectionMethod = (ldUILanguage, ldLocale, ldNone);
   TSetupCompressMethod = (cmStored, cmZip, cmBzip, cmLZMA, cmLZMA2);
   TSetupKDFSalt = array[0..15] of Byte;
@@ -408,8 +409,13 @@ type
     TableCRC: Int32;                { CRC of all prior fields in this record }
   end;
 
-  { TMessagesLangOptions is a simplified version of TSetupLanguageEntry that
-    is used by the uninstaller and RegSvr }
+  { TMessagesLangOptions contains options used by the uninstaller and RegSvr,
+    which are set exclusively by the latest installer, without being affected
+    by any previously installed version. As a result, it is neither backward
+    nor forward compatible, unlike TUninstallLogHeader. Be sure to update
+    MessagesLangOptionsID whenever you make changes to this record. It is
+    named TMessagesLangOptions because it is stored in the Setup.msg file,
+    not because all options must be language-specific. }
   TMessagesLangOptions = packed record
     ID: TMessagesLangOptionsID;
     DialogFontName: array[0..31] of Char;

+ 19 - 7
whatsnew.htm

@@ -46,9 +46,10 @@ For conditions of distribution and use, see <a href="files/is/license.txt">LICEN
 
 <p>Thank you to everyone who has already <a href="https://jrsoftware.org/isorder.php">purchased a commercial license</a>. Your support is very important to our ongoing development.</p>
 
-<span class="head2">Improved wizard images support</span>
-<p>Setup now allows you to specify a custom background color and optionally also a background image. This supports blending the color and images with each other, and optionally supports using a separate background color and image if dark mode is active. Custom background colors are also supported by Uninstall.</p>
-<p>All these changes are backward compatible: if you do not set any of the new directives, then your installers and uninstallers will look the same as before.</p>
+<span class="head2">Custom wizard background colors and images</span>
+<p>Setup now allows you to specify a custom background color and optionally also a background image. This supports blending the color and images with each other, and optionally supports using a separate background color and image if dark mode is active.</p>
+<p>Custom background colors are also supported by Uninstall.</p>
+<p>These changes are backward compatible: if you do not set any of the new directives, then your installers and uninstallers will look the same as before.</p>
 <ul>
   <li>Added new <tt>[Setup]</tt> section directives <tt>WizardBackColor</tt> and <tt>WizardBackColorDynamicDark</tt> to specify custom background colors to use for wizard pages and all other windows such as the <i>Select Language</i> dialog, in both Setup and Uninstall.</li>
   <li>Added new <tt>[Setup]</tt> section directives <tt>WizardBackImageFile</tt> and <tt>WizardBackImageFileDynamicDark</tt> to specify custom images to display as the background of wizard pages in Setup, but not in Uninstall.<br/>
@@ -84,9 +85,22 @@ For conditions of distribution and use, see <a href="files/is/license.txt">LICEN
 <span class="key">WizardBackImageOpacity</span>=150</code></pre></li>
 </ul>
 
-<p><a name="6.6.1"></a><span class="ver">6.6.1 </span><span class="date">(2025-11-19)</span></p>
+<span class="head2">Other changes</span>
+<ul>
+  <li>Security improvement:
+  <ul>
+    <li>Setup and Uninstall now always try to enable <a href="https://www.microsoft.com/en-us/msrc/blog/2025/06/redirectionguard-mitigating-unsafe-junction-traversal-in-windows">Windows RedirectionGuard</a> by default.<br/>
+        This mitigation, currently available only on Windows 11, blocks traversal of NTFS junctions created by non-admin users, to prevent path redirection vulnerabilities when privileged installers touch attacker-controlled directories.<br/>
+        Can be disabled by setting new <tt>[Setup]</tt> section directive <tt>RedirectionGuard</tt> to <tt>no</tt>, and can also be disabled or enabled using new Setup and Uninstall command line parameters '/NOREDIRECTIONGUARD' and '/REDIRECTIONGUARD'.<br/>
+        When no command line parameter is specified, Uninstall enables or disables RedirectionGuard based on the latest install.<br/>
+        RedirectionGuard status is logged, so you can verify whether the mitigation was active.</li>
+  </ul>
+  </li>
+</ul>
 
-<p>Thank you to everyone who has already <a href="https://jrsoftware.org/isorder.php">purchased a commercial license</a>. Your support is very important to our ongoing development.</p>
+<p>Using Inno Setup commercially? Please <a href="https://jrsoftware.org/isorder.php">purchase a license</a>.</p>
+
+<p><a name="6.6.1"></a><span class="ver">6.6.1 </span><span class="date">(2025-11-19)</span></p>
 
 <ul>
   <li>Changes related to custom styles:
@@ -105,8 +119,6 @@ For conditions of distribution and use, see <a href="files/is/license.txt">LICEN
   <li>Other minor improvements.</li>
 </ul>
 
-<p>Using Inno Setup commercially? Please <a href="https://jrsoftware.org/isorder.php">purchase a license</a>.</p>
-
 <p><a name="6.6.0"></a><span class="ver">6.6.0 </span><span class="date">(2025-11-11)</span></p>
 
 <span class="head2">Dark mode and custom styles in Setup and Uninstall</span>