Skip to content

Commit 1c3d880

Browse files
alan-nullmichaellwest
authored andcommitted
#1362 | Security Enhancement (upload file)
(cherry picked from commit 41fac7a)
1 parent 0c8fb7e commit 1c3d880

File tree

7 files changed

+242
-0
lines changed

7 files changed

+242
-0
lines changed

src/Spe/App_Config/Include/Spe/Spe.config

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@
240240
<!--field>__Security</field-->
241241
</ignoredFields>
242242
</translation>
243+
<uploadFile>
244+
<!-- Mime type or extension: .png, image/*, text/csv -->
245+
<allowedFileTypes>
246+
<pattern>image/*</pattern>
247+
</allowedFileTypes>
248+
<allowedLocations>
249+
<!--<path>temp</path>-->
250+
</allowedLocations>
251+
</uploadFile>
243252
</powershell>
244253
<pipelines>
245254
<initialize>

src/Spe/Client/Applications/UploadFile/PowerShellUploadFilePage2.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Sitecore.Shell.Web.UI;
1010
using Sitecore.Web;
1111
using Sitecore.Web.UI.XmlControls;
12+
using Spe.Client.Applications.UploadFile.Validation;
1213
using Spe.Core.Diagnostics;
1314

1415
namespace Spe.Client.Applications.UploadFile
@@ -39,6 +40,16 @@ protected override void OnLoad(EventArgs e)
3940
return;
4041
try
4142
{
43+
string[] patterns = Factory.GetStringSet("powershell/uploadFile/allowedFileTypes/pattern")?.ToArray() ?? new string[] { "image/*" };
44+
var contentTypeValidator = new ContentTypeValidator(patterns);
45+
var result = contentTypeValidator.Validate(Request.Files);
46+
if (!result.Valid)
47+
{
48+
CancelResult();
49+
Sitecore.Diagnostics.Log.Warn($"[SPE] {result.Message}", this);
50+
return;
51+
}
52+
4253
var pathOrId = Sitecore.Context.ClientPage.ClientRequest.Form["ItemUri"];
4354
var langStr = Sitecore.Context.ClientPage.ClientRequest.Form["LanguageName"];
4455
var language = langStr.Length > 0
@@ -57,6 +68,15 @@ protected override void OnLoad(EventArgs e)
5768
{
5869
uploadArgs.Destination = UploadDestination.File;
5970
uploadArgs.FileOnly = true;
71+
string[] allowedLocations = Factory.GetStringSet("powershell/uploadFile/allowedLocations/path").ToArray();
72+
var validator = new UploadLocationValidator(allowedLocations);
73+
pathOrId = validator.GetFullPath(pathOrId);
74+
if (!validator.Validate(pathOrId))
75+
{
76+
CancelResult();
77+
Sitecore.Diagnostics.Log.Warn($"[SPE] Location: '{pathOrId}' is protected. Please configure 'powershell/uploadFile/allowedLocations' if you wish to change it.", this);
78+
return;
79+
}
6080
}
6181
uploadArgs.Files = Request.Files;
6282
uploadArgs.Folder = pathOrId;
@@ -125,5 +145,10 @@ protected override void OnLoad(EventArgs e)
125145
}
126146
}
127147
}
148+
149+
private static void CancelResult()
150+
{
151+
HttpContext.Current.Response.Write("<html><head><script type=\"text/JavaScript\" language=\"javascript\">window.top.scForm.getTopModalDialog().frames[0].scForm.postRequest(\"\", \"\", \"\", 'EndUploading(\"\")')</script></head><body>Done</body></html>");
152+
}
128153
}
129154
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.IO.Compression;
5+
using System.Linq;
6+
using System.Web;
7+
using Sitecore;
8+
using Sitecore.Diagnostics;
9+
using Sitecore.Globalization;
10+
using Sitecore.Pipelines.Upload;
11+
using Sitecore.StringExtensions;
12+
13+
namespace Spe.Client.Applications.UploadFile.Validation
14+
{
15+
internal class ContentTypeValidator
16+
{
17+
internal IReadOnlyCollection<FileTypeValidator> validators { get; }
18+
19+
public ContentTypeValidator(string[] patterns)
20+
{
21+
validators = CreateValidators(patterns);
22+
}
23+
24+
public ValidationResult Validate(HttpFileCollection Files)
25+
{
26+
if (!validators.Any())
27+
{
28+
return new ValidationResult { Message = string.Empty, Valid = true };
29+
}
30+
31+
foreach (string key in Files)
32+
{
33+
var file = Files[key];
34+
35+
if (file == null)
36+
{
37+
continue;
38+
}
39+
40+
if (!IsFileAccepted(file, validators))
41+
{
42+
var reason = Translate.Text("File type isn`t allowed.");
43+
reason = StringUtil.EscapeJavascriptString(reason);
44+
var convertedFileName = StringUtil.EscapeJavascriptString(file.FileName);
45+
46+
var errorText = Translate.Text(string.Format("The '{0}' file cannot be uploaded. File type isn`t allowed.", file.FileName));
47+
Log.Warn(errorText, this);
48+
return new ValidationResult { Message = errorText, Valid = false };
49+
}
50+
}
51+
return new ValidationResult { Message = string.Empty, Valid = true };
52+
}
53+
54+
protected static bool IsUnpack(HttpPostedFileBase file)
55+
{
56+
return string.Compare(Path.GetExtension(file.FileName), ".zip", StringComparison.InvariantCultureIgnoreCase) == 0;
57+
}
58+
59+
private static bool IsFileAccepted(HttpPostedFile file, IReadOnlyCollection<FileTypeValidator> validators)
60+
{
61+
if (string.IsNullOrEmpty(file.FileName))
62+
{
63+
return true;
64+
}
65+
66+
var isArchive = IsUnpack(new HttpPostedFileWrapper(file));
67+
if (!isArchive)
68+
{
69+
return validators.Any(x => x.IsValid(file.FileName));
70+
}
71+
72+
73+
if (file.InputStream.Position != 0)
74+
{
75+
file.InputStream.Position = 0;
76+
}
77+
78+
var archive = new ZipArchive(file.InputStream, ZipArchiveMode.Read, true);
79+
try
80+
{
81+
return archive.Entries
82+
.Where(entry => !entry.FullName.EndsWith("/"))
83+
.All(entry => validators.Any(x => x.IsValid(entry.FullName)));
84+
}
85+
finally
86+
{
87+
archive.Dispose();
88+
if (file.InputStream.Position != 0)
89+
{
90+
file.InputStream.Position = 0;
91+
}
92+
}
93+
}
94+
95+
private static IReadOnlyCollection<FileTypeValidator> CreateValidators(string[] allowedFileTypes)
96+
{
97+
if (!allowedFileTypes.Any())
98+
{
99+
return new List<FileTypeValidator>();
100+
}
101+
102+
return allowedFileTypes
103+
.Select(p => p.Trim())
104+
.Select(p => new FileTypeValidator(p))
105+
.ToList();
106+
}
107+
}
108+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Linq;
3+
using System.Web;
4+
5+
namespace Spe.Client.Applications.UploadFile.Validation
6+
{
7+
internal class FileTypeValidator
8+
{
9+
private readonly Func<string, bool> _validate;
10+
11+
public FileTypeValidator(string pattern)
12+
{
13+
if (pattern.Contains('.'))
14+
{
15+
_validate = fileName => fileName.EndsWith(pattern);
16+
}
17+
else if (pattern.Contains("/*"))
18+
{
19+
_validate = fileName => MimeMapping.GetMimeMapping(fileName).Split('/').FirstOrDefault() == pattern.Split('/').First();
20+
}
21+
else if (pattern.Contains("/"))
22+
{
23+
_validate = fileName => MimeMapping.GetMimeMapping(fileName) == pattern;
24+
}
25+
else
26+
{
27+
throw new NotSupportedException("Pattern isn't supported");
28+
}
29+
}
30+
31+
public bool IsValid(string fileName)
32+
{
33+
return _validate(fileName);
34+
}
35+
}
36+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.IO.Compression;
5+
using System.Linq;
6+
using System.Web;
7+
using Sitecore;
8+
using Sitecore.Diagnostics;
9+
using Sitecore.Globalization;
10+
using Sitecore.Pipelines.Upload;
11+
using Sitecore.StringExtensions;
12+
13+
namespace Spe.Client.Applications.UploadFile.Validation
14+
{
15+
internal class UploadLocationValidator
16+
{
17+
private readonly List<string> _allowedLocations;
18+
private readonly string _webRootPath;
19+
20+
public UploadLocationValidator(IEnumerable<string> allowedLocations)
21+
{
22+
_webRootPath = HttpContext.Current.Server.MapPath("\\");
23+
24+
// Convert relative paths to absolute paths
25+
_allowedLocations = allowedLocations
26+
.Select(path => Path.GetFullPath(Path.IsPathRooted(path) ? path : Path.Combine(_webRootPath, path)))
27+
.ToList();
28+
}
29+
30+
public bool Validate(string userDefinedPath)
31+
{
32+
if (string.IsNullOrWhiteSpace(userDefinedPath)) return false;
33+
34+
string fullPath;
35+
try
36+
{
37+
fullPath = GetFullPath(userDefinedPath);
38+
}
39+
catch (Exception)
40+
{
41+
return false; // Invalid path format
42+
}
43+
44+
return _allowedLocations.Any(allowedPath => fullPath.StartsWith(allowedPath, StringComparison.OrdinalIgnoreCase));
45+
}
46+
47+
public string GetFullPath(string path)
48+
{
49+
return Path.GetFullPath(Path.IsPathRooted(path) ? path : Path.Combine(_webRootPath, path));
50+
}
51+
}
52+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Spe.Client.Applications.UploadFile.Validation
2+
{
3+
internal class ValidationResult
4+
{
5+
public string Message { get; set; }
6+
public bool Valid { get; set; }
7+
}
8+
}

src/Spe/Spe.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@
178178
<Compile Include="Client\Applications\PowerShellReports.cs" />
179179
<Compile Include="Client\Applications\PowerShellSessionElevation.cs" />
180180
<Compile Include="Client\Applications\SessionElevationWindowLauncher.cs" />
181+
<Compile Include="Client\Applications\UploadFile\Validation\UploadLocationValidator.cs" />
182+
<Compile Include="Client\Applications\UploadFile\Validation\FileTypeValidator.cs" />
183+
<Compile Include="Client\Applications\UploadFile\Validation\ContentTypeValidator.cs" />
184+
<Compile Include="Client\Applications\UploadFile\Validation\ValidationResult.cs" />
181185
<Compile Include="Client\Commands\EditIseSettings.cs" />
182186
<Compile Include="Client\Commands\ExecuteFieldEditor.cs" />
183187
<Compile Include="Client\Commands\MenuItems\AddMaster.cs" />

0 commit comments

Comments
 (0)