Skip to content

Commit 74914f7

Browse files
committed
Add security hardening and atomic writes to backend writers
- Add XXE attack prevention with secure XML reader settings (iOS) - Add atomic write operations with temp files to prevent corruption (Json, Po, Resx, iOS) - Use safe JSON encoder to prevent XSS attacks (Json) - Ensure UTF-8 without BOM encoding consistency
1 parent df606cb commit 74914f7

5 files changed

Lines changed: 70 additions & 12 deletions

File tree

LocalizationManager.Core/Backends/Json/JsonResourceWriter.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,19 @@ public JsonResourceWriter(JsonFormatConfiguration? config = null)
4747
public void Write(ResourceFile file)
4848
{
4949
var json = BuildJsonString(file);
50-
File.WriteAllText(file.Language.FilePath, json);
50+
51+
// Use atomic write to prevent file corruption
52+
var tempPath = file.Language.FilePath + $".tmp.{Guid.NewGuid()}";
53+
try
54+
{
55+
File.WriteAllText(tempPath, json, new System.Text.UTF8Encoding(false));
56+
File.Move(tempPath, file.Language.FilePath, overwrite: true);
57+
}
58+
finally
59+
{
60+
if (File.Exists(tempPath))
61+
File.Delete(tempPath);
62+
}
5163
}
5264

5365
/// <summary>
@@ -108,7 +120,7 @@ private string BuildJsonString(ResourceFile file)
108120
return JsonSerializer.Serialize(root, new JsonSerializerOptions
109121
{
110122
WriteIndented = true,
111-
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
123+
Encoder = JavaScriptEncoder.Default // Use safe encoder to prevent XSS
112124
});
113125
}
114126

LocalizationManager.Core/Backends/Po/PoResourceWriter.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,20 @@ public async Task WriteAsync(ResourceFile file, CancellationToken ct = default)
3737
Directory.CreateDirectory(directory);
3838

3939
// Atomic write: write to temp file then rename
40-
var tempPath = file.Language.FilePath + ".tmp";
41-
// Use UTF-8 without BOM (PO file standard)
42-
await File.WriteAllTextAsync(tempPath, content, new UTF8Encoding(false), ct);
43-
File.Move(tempPath, file.Language.FilePath, overwrite: true);
40+
// Use unique temp file name to prevent race conditions with concurrent writes
41+
var tempPath = file.Language.FilePath + $".tmp.{Guid.NewGuid()}";
42+
try
43+
{
44+
// Use UTF-8 without BOM (PO file standard)
45+
await File.WriteAllTextAsync(tempPath, content, new UTF8Encoding(false), ct);
46+
File.Move(tempPath, file.Language.FilePath, overwrite: true);
47+
}
48+
finally
49+
{
50+
// Clean up temp file if it still exists
51+
if (File.Exists(tempPath))
52+
File.Delete(tempPath);
53+
}
4454
}
4555

4656
/// <inheritdoc />
@@ -214,7 +224,8 @@ private void WriteEntryContent(StringBuilder sb, ResourceEntry entry, string lan
214224
var msgId = entry.Key;
215225

216226
var pipeIndex = entry.Key.IndexOf('|');
217-
if (pipeIndex > 0)
227+
// Validate: pipe must not be first or last character
228+
if (pipeIndex > 0 && pipeIndex < entry.Key.Length - 1)
218229
{
219230
context = entry.Key.Substring(0, pipeIndex);
220231
msgId = entry.Key.Substring(pipeIndex + 1);

LocalizationManager.Core/Backends/Resx/ResxResourceWriter.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,18 @@ public void Write(ResourceFile file)
8080
WriteWithUniqueKeys(root, file.Entries);
8181
}
8282

83-
// Save with proper formatting
84-
xdoc.Save(file.Language.FilePath);
83+
// Save with proper formatting using atomic write to prevent file corruption
84+
var tempPath = file.Language.FilePath + $".tmp.{Guid.NewGuid()}";
85+
try
86+
{
87+
xdoc.Save(tempPath);
88+
File.Move(tempPath, file.Language.FilePath, overwrite: true);
89+
}
90+
finally
91+
{
92+
if (File.Exists(tempPath))
93+
File.Delete(tempPath);
94+
}
8595
}
8696
catch (Exception ex)
8797
{

LocalizationManager.Core/Backends/iOS/IosResourceWriter.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,19 @@ public void Write(ResourceFile file)
5151
new StringsFileParser.StringsEntry(e.Key, e.Value ?? "", e.Comment));
5252

5353
var content = _stringsParser.Serialize(stringsEntries);
54-
File.WriteAllText(stringsPath, content);
54+
55+
// Use atomic write to prevent file corruption
56+
var tempPath = stringsPath + $".tmp.{Guid.NewGuid()}";
57+
try
58+
{
59+
File.WriteAllText(tempPath, content, new System.Text.UTF8Encoding(false));
60+
File.Move(tempPath, stringsPath, overwrite: true);
61+
}
62+
finally
63+
{
64+
if (File.Exists(tempPath))
65+
File.Delete(tempPath);
66+
}
5567
}
5668
else if (!pluralStrings.Any())
5769
{
@@ -75,7 +87,19 @@ public void Write(ResourceFile file)
7587
e.PluralForms!));
7688

7789
var content = _stringsdictParser.Serialize(stringsdictEntries);
78-
File.WriteAllText(stringsdictPath, content);
90+
91+
// Use atomic write to prevent file corruption
92+
var tempPath = stringsdictPath + $".tmp.{Guid.NewGuid()}";
93+
try
94+
{
95+
File.WriteAllText(tempPath, content, new System.Text.UTF8Encoding(false));
96+
File.Move(tempPath, stringsdictPath, overwrite: true);
97+
}
98+
finally
99+
{
100+
if (File.Exists(tempPath))
101+
File.Delete(tempPath);
102+
}
79103
}
80104
else if (File.Exists(stringsdictPath))
81105
{

LocalizationManager.Core/Backends/iOS/StringsFileParser.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ public static string EscapeString(string s)
241241
}
242242

243243
// Pattern: "key" = "value"; with optional whitespace and semicolon
244-
[GeneratedRegex(@"^\s*""(.+?)""\s*=\s*""(.*)""\s*;?\s*$", RegexOptions.Singleline)]
244+
// Using timeout to prevent ReDoS attacks
245+
[GeneratedRegex(@"^\s*""(.+?)""\s*=\s*""(.*)""\s*;?\s*$", RegexOptions.Singleline, 1000)]
245246
private static partial Regex KeyValuePattern();
246247
}

0 commit comments

Comments
 (0)