forked from JimBobSquarePants/ImageProcessor
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRemoteFile.cs
More file actions
177 lines (159 loc) · 7.64 KB
/
Copy pathRemoteFile.cs
File metadata and controls
177 lines (159 loc) · 7.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="RemoteFile.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Encapsulates methods used to download files from a website address.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Security;
using System.Threading.Tasks;
using System.Web;
using ImageProcessor.Configuration;
using ImageProcessor.Web.HttpModules;
namespace ImageProcessor.Web.Helpers
{
/// <summary>
/// Encapsulates methods used to download files from a website address.
/// </summary>
/// <remarks>
/// <para>
/// The purpose of this class is so there's one core way of downloading remote files with url[s] that are from
/// outside users. There's various areas in application where an attacker could supply an external url to the server
/// and tie up resources.
/// </para>
/// For example, the ImageProcessingModule accepts off-server addresses as a path. An attacker could, for instance, pass the url
/// to a file that's a few gigs in size, causing the server to get out-of-memory exceptions or some other errors. An attacker
/// could also use this same method to use one application instance to hammer another site by, again, passing an off-server
/// address of the victims site to the <see cref="ImageProcessingModule"/>.
/// This class will not throw an exception if the Uri supplied points to a resource local to the running application instance.
/// <para>
/// There shouldn't be any security issues there, as the internal <see cref="HttpClient"/> instance is still calling it remotely.
/// Any local files that shouldn't be accessed by this won't be allowed by the remote call.
/// </para>
/// </remarks>
internal sealed class RemoteFile
{
private static readonly HttpClientHandler Handler = new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
Credentials = CredentialCache.DefaultNetworkCredentials
};
private static readonly HttpClient Client = new HttpClient(Handler);
/// <summary>
/// Initializes a new instance of the <see cref="RemoteFile"/> class.
/// </summary>
/// <param name="timeoutMilliseconds">The maximum time, in milliseconds, to wait before the request times out.</param>
/// <param name="maxDownloadSize">The maximum download size, in bytes, that a remote file download attempt can be.</param>
public RemoteFile(int timeoutMilliseconds, int maxDownloadSize)
: this(timeoutMilliseconds, maxDownloadSize, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RemoteFile"/> class.
/// </summary>
/// <param name="timeoutMilliseconds">The maximum time, in milliseconds, to wait before the request times out.</param>
/// <param name="maxDownloadSize">The maximum download size, in bytes, that a remote file download attempt can be.</param>
/// <param name="userAgent">The User-Agent header to be passed when requesting the remote file.</param>
public RemoteFile(int timeoutMilliseconds, int maxDownloadSize, string userAgent)
{
if (timeoutMilliseconds >= 0)
{
this.Timeout = TimeSpan.FromMilliseconds(timeoutMilliseconds);
}
if (maxDownloadSize > 0)
{
this.MaxDownloadSize = maxDownloadSize;
}
this.UserAgent = userAgent;
// We're reusing the same static HttpClient so we don't exhaust the number of sockets available under heavy loads.
// We're always using the same timeout, and user-agent values per all instances so it's ok to set the values here per instance.
Client.Timeout = this.Timeout;
var key = "User-Agent";
if (!string.IsNullOrWhiteSpace(this.UserAgent) && !Client.DefaultRequestHeaders.TryGetValues(key, out var _))
{
Client.DefaultRequestHeaders.Add(key, this.UserAgent);
}
}
/// <summary>
/// Gets or sets the timespan to wait before the request times out.
/// </summary>
public TimeSpan Timeout { get; } = TimeSpan.FromSeconds(100.0);
/// <summary>
/// Gets the maximum download size, in bytes, that a remote file download attempt can be.
/// </summary>
public int MaxDownloadSize { get; } = int.MaxValue;
/// <summary>
/// Gets the UserAgent header to be passed when requesting the remote file
/// </summary>
public string UserAgent { get; }
/// <summary>
/// Returns the <see cref="HttpResponseMessage"/> used to download this file.
/// <remarks>
/// <para>
/// This method is meant for outside users who need specific access to the HttpResponseMessage this class
/// generates. They're responsible for disposing of it.
/// </para>
/// </remarks>
/// </summary>
/// <returns>The <see cref="HttpResponseMessage"/> used to download this file.</returns>
/// <returns>
/// The <see cref="IEnumerable{T}"/>.
/// </returns>
internal async Task<HttpResponseMessage> GetResponseAsync(Uri uri)
{
void Log(HttpRequestException ex)
{
ImageProcessorBootstrapper.Instance.Logger.Log<RemoteFile>(ex.Message);
}
HttpResponseMessage response = null;
HttpStatusCode statusCode = HttpStatusCode.OK;
try
{
response = await Client.GetAsync(uri).ConfigureAwait(false);
statusCode = response.StatusCode;
response.EnsureSuccessStatusCode();
long? contentLength = response.Content.Headers.ContentLength;
if (contentLength.HasValue)
{
if (contentLength > this.MaxDownloadSize)
{
response.Dispose();
string message = "An attempt to download a remote file has been halted because the file is larger than allowed.";
ImageProcessorBootstrapper.Instance.Logger.Log<RemoteFile>(message);
throw new SecurityException(message);
}
}
else
{
response.Dispose();
throw new HttpException((int)HttpStatusCode.NotFound, $"No image exists at {uri}");
}
return response;
}
catch (HttpRequestException ex)
{
switch (statusCode)
{
// No error, carry on.
case HttpStatusCode.NotModified:
break;
case HttpStatusCode.NotFound:
// We want 404's to be handled by IIS so that other handlers/modules can still run.
Log(ex);
response?.Dispose();
throw new HttpException((int)statusCode, $"No image exists at {uri}", ex);
default:
Log(ex);
break;
}
}
return null;
}
}
}