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
185 lines (166 loc) · 8.1 KB
/
Copy pathRemoteFile.cs
File metadata and controls
185 lines (166 loc) · 8.1 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
178
179
180
181
182
183
184
185
// --------------------------------------------------------------------------------------------------------------------
// <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 and won't be allowed by the remote call.
/// </para>
/// </remarks>
internal sealed class RemoteFile
{
/// <summary>
/// The default message handler used by HttpClient instances.
/// </summary>
public static readonly HttpClientHandler Handler = new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
Credentials = CredentialCache.DefaultNetworkCredentials
};
private readonly HttpClient client;
/// <summary>
/// Initializes a new instance of the <see cref="RemoteFile"/> class.
/// </summary>
/// <param name="client">The client used for creating the web request.</param>
/// <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(HttpClient client, int timeoutMilliseconds, int maxDownloadSize)
: this(client, timeoutMilliseconds, maxDownloadSize, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RemoteFile"/> class.
/// </summary>
/// <param name="client">The client used for creating the web request.</param>
/// <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(HttpClient client, int timeoutMilliseconds, int maxDownloadSize, string userAgent)
{
this.client = client ?? new HttpClient(Handler);
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.
this.client.Timeout = this.Timeout;
const string key = "User-Agent";
if (!string.IsNullOrWhiteSpace(this.UserAgent) && !this.client.DefaultRequestHeaders.TryGetValues(key, out IEnumerable<string> _))
{
this.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>
/// <param name="uri">The request uri.</param>
/// <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 this.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();
const 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;
}
}
}