Skip to content

Commit 9752df3

Browse files
a2580vbliccode
andauthored
feature: confirm before trusting a repository refused by git (#2746)
* feature: confirm before trusting a repository refused by git * refactor: keep only permanent trust for untrusted repositories --------- Co-authored-by: liccode <a258vb@163.com>
1 parent d51cd41 commit 9752df3

18 files changed

Lines changed: 403 additions & 15 deletions

‎src/App.axaml.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ private bool TryLaunchAsFileHistoryViewer(IClassicDesktopStyleApplicationLifetim
350350
var test = new Commands.QueryRepositoryRootPath(dir).GetResult();
351351
if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut))
352352
{
353-
Console.Out.WriteLine($"'{args[1]}' is not in a valid git repository");
353+
Console.Out.WriteLine(string.IsNullOrWhiteSpace(test.StdErr) ? $"'{args[1]}' is not in a valid git repository" : test.StdErr.Trim());
354354
desktop.Shutdown(-1);
355355
return true;
356356
}
@@ -395,7 +395,7 @@ private bool TryLaunchAsBlameViewer(IClassicDesktopStyleApplicationLifetime desk
395395
var test = new Commands.QueryRepositoryRootPath(dir).GetResult();
396396
if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut))
397397
{
398-
Console.Out.WriteLine($"'{args[1]}' is not in a valid git repository");
398+
Console.Out.WriteLine(string.IsNullOrWhiteSpace(test.StdErr) ? $"'{args[1]}' is not in a valid git repository" : test.StdErr.Trim());
399399
desktop.Shutdown(-1);
400400
return true;
401401
}

‎src/Commands/AddSafeDirectory.cs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace SourceGit.Commands
2+
{
3+
public class AddSafeDirectory : Command
4+
{
5+
public AddSafeDirectory(string ctx, string value)
6+
{
7+
Context = ctx;
8+
Args = $"config --global --add safe.directory {value.Quoted()}";
9+
}
10+
}
11+
}

‎src/Commands/Command.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,8 @@ protected ProcessStartInfo CreateGitStartInfo(bool redirect)
210210
if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey))
211211
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}' -o AddKeysToAgent=yes");
212212

213-
// Force using en_US.UTF-8 locale
214-
if (OperatingSystem.IsLinux())
213+
// Force the C locale on Unix, so that git's output (including fatal errors) can always be parsed.
214+
if (!OperatingSystem.IsWindows())
215215
{
216216
start.Environment.Add("LANG", "C");
217217
start.Environment.Add("LC_ALL", "C");

‎src/Models/SafeDirectories.cs‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System;
2+
using System.Text;
3+
using System.Text.RegularExpressions;
4+
5+
namespace SourceGit.Models
6+
{
7+
/// <summary>
8+
/// Helpers for the `safe.directory` protection introduced by git 2.35.2 (CVE-2022-24765).
9+
/// Git refuses to use a repository whose top-level directory is owned by another user (which is
10+
/// always the case for network shares / UNC paths) unless the path is listed in the `safe.directory` config.
11+
/// </summary>
12+
public static partial class SafeDirectories
13+
{
14+
public static bool IsUntrustedRepository(string output)
15+
{
16+
if (string.IsNullOrWhiteSpace(output))
17+
return false;
18+
19+
return output.Contains("detected dubious ownership", StringComparison.OrdinalIgnoreCase) ||
20+
output.Contains("unsafe repository", StringComparison.OrdinalIgnoreCase);
21+
}
22+
23+
/// <summary>
24+
/// Tries to get the value that should be written into the `safe.directory` config.
25+
/// Prefers the value suggested by git itself (which knows the correct form for the current platform),
26+
/// and falls back to building a value from the given path.
27+
/// </summary>
28+
public static bool TryGetSafeDirectoryValue(string path, string output, out string value)
29+
{
30+
value = ParseSuggestedValue(output);
31+
if (!string.IsNullOrEmpty(value))
32+
return true;
33+
34+
var normalized = Normalize(path);
35+
if (string.IsNullOrEmpty(normalized))
36+
return false;
37+
38+
// Git for Windows uses the `%(prefix)/` prefix for UNC paths.
39+
value = OperatingSystem.IsWindows() && normalized.StartsWith("//", StringComparison.Ordinal) ? $"%(prefix)/{normalized}" : normalized;
40+
return true;
41+
}
42+
43+
private static string Normalize(string path)
44+
{
45+
return path?.Replace('\\', '/').TrimEnd('/') ?? string.Empty;
46+
}
47+
48+
private static string ParseSuggestedValue(string output)
49+
{
50+
if (string.IsNullOrEmpty(output))
51+
return string.Empty;
52+
53+
var match = REG_SAFE_DIRECTORY_HINT().Match(output);
54+
if (!match.Success)
55+
return string.Empty;
56+
57+
return Dequote(match.Groups["value"].Value.Trim());
58+
}
59+
60+
/// <summary>
61+
/// Git quotes the suggested value with `sq_quote_buf()`, which escapes `'` and `!` as `'\''` and
62+
/// `'\!'`. Decodes it back to the plain path (see `quote.c` of git).
63+
/// </summary>
64+
private static string Dequote(string value)
65+
{
66+
if (value.Length > 1 && value[0] == '\'')
67+
{
68+
var builder = new StringBuilder(value.Length);
69+
for (var i = 1; i < value.Length; i++)
70+
{
71+
var c = value[i];
72+
if (c != '\'')
73+
{
74+
builder.Append(c);
75+
continue;
76+
}
77+
78+
if (i == value.Length - 1)
79+
return builder.ToString();
80+
81+
if (i + 3 < value.Length && value[i + 1] == '\\' && (value[i + 2] == '\'' || value[i + 2] == '!') && value[i + 3] == '\'')
82+
{
83+
builder.Append(value[i + 2]);
84+
i += 3;
85+
continue;
86+
}
87+
88+
return string.Empty;
89+
}
90+
91+
return string.Empty;
92+
}
93+
94+
if (value.Length > 1 && value[0] == '"' && value[^1] == '"')
95+
return value[1..^1];
96+
97+
return value;
98+
}
99+
100+
[GeneratedRegex(@"--add\s+safe\.directory\s+(?<value>[^\r\n]+)", RegexOptions.Multiline)]
101+
private static partial Regex REG_SAFE_DIRECTORY_HINT();
102+
}
103+
}

‎src/Resources/Locales/en_US.axaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,12 @@
997997
<x:String x:Key="Text.TagCM.Merge" xml:space="preserve">Merge ${0}$ into ${1}$...</x:String>
998998
<x:String x:Key="Text.TagCM.Push" xml:space="preserve">Push ${0}$...</x:String>
999999
<x:String x:Key="Text.Terminate" xml:space="preserve">TERMINATE</x:String>
1000+
<x:String x:Key="Text.TrustRepository" xml:space="preserve">Untrusted Repository</x:String>
1001+
<x:String x:Key="Text.TrustRepository.CommandTip" xml:space="preserve">Will run: </x:String>
1002+
<x:String x:Key="Text.TrustRepository.Description" xml:space="preserve">Git refused to access this repository: </x:String>
1003+
<x:String x:Key="Text.TrustRepository.Path" xml:space="preserve">Path:</x:String>
1004+
<x:String x:Key="Text.TrustRepository.Permanent" xml:space="preserve">Trust this repository</x:String>
1005+
<x:String x:Key="Text.TrustRepository.ScanSkipped" xml:space="preserve">{0} repositories were skipped because git does not trust them. Please open them manually and confirm whether to trust them.</x:String>
10001006
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">Update Submodules</x:String>
10011007
<x:String x:Key="Text.UpdateSubmodules.All" xml:space="preserve">All submodules</x:String>
10021008
<x:String x:Key="Text.UpdateSubmodules.Init" xml:space="preserve">Initialize as needed</x:String>

‎src/Resources/Locales/zh_CN.axaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,12 @@
10011001
<x:String x:Key="Text.TagCM.Merge" xml:space="preserve">合并 ${0}$ 到 ${1}$...</x:String>
10021002
<x:String x:Key="Text.TagCM.Push" xml:space="preserve">推送 ${0}$...</x:String>
10031003
<x:String x:Key="Text.Terminate" xml:space="preserve">终止运行</x:String>
1004+
<x:String x:Key="Text.TrustRepository" xml:space="preserve">不受信任的仓库</x:String>
1005+
<x:String x:Key="Text.TrustRepository.CommandTip" xml:space="preserve">将执行:</x:String>
1006+
<x:String x:Key="Text.TrustRepository.Description" xml:space="preserve">Git 拒绝访问该仓库: </x:String>
1007+
<x:String x:Key="Text.TrustRepository.Path" xml:space="preserve">路径 :</x:String>
1008+
<x:String x:Key="Text.TrustRepository.Permanent" xml:space="preserve">信任此仓库</x:String>
1009+
<x:String x:Key="Text.TrustRepository.ScanSkipped" xml:space="preserve">有 {0} 个仓库因 git 的信任检查被跳过,请手动打开它们并确认是否信任。</x:String>
10041010
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">更新子模块</x:String>
10051011
<x:String x:Key="Text.UpdateSubmodules.All" xml:space="preserve">更新所有子模块</x:String>
10061012
<x:String x:Key="Text.UpdateSubmodules.Init" xml:space="preserve">如未初始化子模块,先初始化</x:String>

‎src/Resources/Locales/zh_TW.axaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,12 @@
991991
<x:String x:Key="Text.TagCM.Merge" xml:space="preserve">合併 ${0}$ 到 ${1}$...</x:String>
992992
<x:String x:Key="Text.TagCM.Push" xml:space="preserve">推送 ${0}$...</x:String>
993993
<x:String x:Key="Text.Terminate" xml:space="preserve">終止執行</x:String>
994+
<x:String x:Key="Text.TrustRepository" xml:space="preserve">不受信任的存放庫</x:String>
995+
<x:String x:Key="Text.TrustRepository.CommandTip" xml:space="preserve">將執行:</x:String>
996+
<x:String x:Key="Text.TrustRepository.Description" xml:space="preserve">Git 拒絕存取該存放庫: </x:String>
997+
<x:String x:Key="Text.TrustRepository.Path" xml:space="preserve">路徑 :</x:String>
998+
<x:String x:Key="Text.TrustRepository.Permanent" xml:space="preserve">信任此存放庫</x:String>
999+
<x:String x:Key="Text.TrustRepository.ScanSkipped" xml:space="preserve">有 {0} 個存放庫因 git 的信任檢查而被略過,請手動開啟並確認是否信任。</x:String>
9941000
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">更新子模組</x:String>
9951001
<x:String x:Key="Text.UpdateSubmodules.All" xml:space="preserve">更新所有子模組</x:String>
9961002
<x:String x:Key="Text.UpdateSubmodules.Init" xml:space="preserve">如果子模組尚未初始化,則將其初始化</x:String>

‎src/ViewModels/Launcher.cs‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,12 @@ public bool TryOpenRepositoryFromPath(string repo)
104104
if (ActivePage is not { Data: Welcome { }, Popup: null })
105105
AddNewTab();
106106

107-
ActivePage.Popup = new Init(ActivePage.Node.Id, repo, null, 0, test.StdErr ?? "Unknown error occurred while opening the repository.");
107+
if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr) &&
108+
Models.SafeDirectories.TryGetSafeDirectoryValue(repo, test.StdErr, out var safeDirectory))
109+
ActivePage.Popup = new TrustRepository(ActivePage.Node.Id, repo, test.StdErr, safeDirectory, null, false, true, 0);
110+
else
111+
ActivePage.Popup = new Init(ActivePage.Node.Id, repo, null, 0, test.StdErr ?? "Unknown error occurred while opening the repository.");
112+
108113
return true;
109114
}
110115
}
@@ -325,6 +330,9 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page)
325330
var gitDir = isBare ? node.Id : GetRepositoryGitDir(node.Id);
326331
if (string.IsNullOrEmpty(gitDir))
327332
{
333+
if (TryShowTrustRepositoryPopup(node.Id, ActivePage))
334+
return;
335+
328336
ActivePage.Notifications.Add(new Models.Notification
329337
{
330338
Group = node.Id,
@@ -404,6 +412,9 @@ public void OpenSubRepository(LauncherPage ownerPage, string fullpath)
404412
var gitDir = GetRepositoryGitDir(normalizedPath);
405413
if (string.IsNullOrEmpty(gitDir))
406414
{
415+
if (TryShowTrustRepositoryPopup(normalizedPath, ownerPage))
416+
return;
417+
407418
ownerPage.Notifications.Add(new Models.Notification
408419
{
409420
Group = ownerPage.Node.Id,
@@ -439,6 +450,21 @@ public void OpenSubRepository(LauncherPage ownerPage, string fullpath)
439450
ActivePage = page;
440451
}
441452

453+
private bool TryShowTrustRepositoryPopup(string path, LauncherPage page)
454+
{
455+
if (page == null || !page.CanCreatePopup())
456+
return false;
457+
458+
var test = new Commands.QueryRepositoryRootPath(path).GetResult();
459+
if (test.IsSuccess ||
460+
!Models.SafeDirectories.IsUntrustedRepository(test.StdErr) ||
461+
!Models.SafeDirectories.TryGetSafeDirectoryValue(path, test.StdErr, out var safeDirectory))
462+
return false;
463+
464+
page.Popup = new TrustRepository(page.Node.Id, path, test.StdErr, safeDirectory, null, false, true, 0);
465+
return true;
466+
}
467+
442468
private void DispatchNotification(Models.Notification notification)
443469
{
444470
if (!Dispatcher.UIThread.CheckAccess())

‎src/ViewModels/LauncherPage.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public bool CanCreatePopup()
8080

8181
public async Task ProcessPopupAsync()
8282
{
83-
if (_popup is { InProgress: false } dump)
83+
if (_popup is { InProgress: false } dump && dump.CanSure)
8484
{
8585
if (!dump.Check())
8686
return;

‎src/ViewModels/OpenLocalRepository.cs‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,27 @@ public override async Task<bool> Sure()
8989
else
9090
{
9191
var launcher = App.GetLauncher();
92-
foreach (var page in launcher.Pages)
92+
if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr) &&
93+
Models.SafeDirectories.TryGetSafeDirectoryValue(_repoPath, test.StdErr, out var safeDirectory))
9394
{
94-
if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal))
95+
foreach (var page in launcher.Pages)
9596
{
96-
page.Popup = new Init(page.Node.Id, _repoPath, parent, _bookmark, test.StdErr);
97-
break;
97+
if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal))
98+
{
99+
page.Popup = new TrustRepository(page.Node.Id, _repoPath, test.StdErr, safeDirectory, parent, true, true, _bookmark);
100+
break;
101+
}
102+
}
103+
}
104+
else
105+
{
106+
foreach (var page in launcher.Pages)
107+
{
108+
if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal))
109+
{
110+
page.Popup = new Init(page.Node.Id, _repoPath, parent, _bookmark, test.StdErr);
111+
break;
112+
}
98113
}
99114
}
100115

0 commit comments

Comments
 (0)