// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // // Encapsulates methods used to download files from a website address. // // -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Web.Helpers { #region Using using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Security; using System.Text; using System.Threading.Tasks; using ImageProcessor.Web.Configuration; #endregion /// /// Encapsulates methods used to download files from a website address. /// /// /// /// 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. /// /// 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 ImageProcessingModule. /// This class will not throw an exception if the Uri supplied points to a resource local to the running application instance. /// /// There shouldn't be any security issues there, as the internal WebRequest instance is still calling it remotely. /// Any local files that shouldn't be accessed by this won't be allowed by the remote call. /// /// Adapted from BlogEngine.Net /// internal sealed class RemoteFile { #region Fields /// /// The white-list of url[s] from which to download remote files. /// public static readonly ImageSecuritySection.SafeUrl[] RemoteFileWhiteListExtensions = ImageProcessorConfiguration.Instance.RemoteFileWhiteListExtensions; /// /// The white-list of url[s] from which to download remote files. /// private static readonly Uri[] RemoteFileWhiteList = ImageProcessorConfiguration.Instance.RemoteFileWhiteList; /// /// The length of time, in milliseconds, that a remote file download attempt can last before timing out. /// private static readonly int TimeoutMilliseconds = ImageProcessorConfiguration.Instance.Timeout; /// /// The maximum size, in bytes, that a remote file download attempt can download. /// private static readonly int MaxBytes = ImageProcessorConfiguration.Instance.MaxBytes; /// /// Whether to allow remote downloads. /// private static readonly bool AllowRemoteDownloads = ImageProcessorConfiguration.Instance.AllowRemoteDownloads; /// /// Whether this RemoteFile instance is ignoring remote download rules set in the current application /// instance. /// private readonly bool ignoreRemoteDownloadSettings; /// /// The Uri of the remote file being downloaded. /// private readonly Uri url; /// /// The maximum allowable download size in bytes. /// private readonly int maxDownloadSize; /// /// The length of time, in milliseconds, that a remote file download attempt can last before timing out. /// private int timeoutLength; /// /// The WebResponse object used internally for this RemoteFile instance. /// private WebRequest webRequest; #endregion #region Constructors /// /// Initializes a new instance of the RemoteFile class. /// /// The url of the file to be downloaded. /// /// If set to , then RemoteFile should ignore the current the applications instance's remote download settings; otherwise,. /// internal RemoteFile(Uri filePath, bool ignoreRemoteDownloadSettings) { if (filePath == null) { throw new ArgumentNullException("filePath"); } this.url = filePath; this.ignoreRemoteDownloadSettings = ignoreRemoteDownloadSettings; this.timeoutLength = TimeoutMilliseconds; this.maxDownloadSize = MaxBytes; } #endregion #region Properties /// /// Gets a value indicating whether this RemoteFile instance is ignoring remote download rules set in the /// current application instance. /// /// This should only be set to true if the supplied url is a verified resource. Use at your own risk. /// /// /// /// if this RemoteFile instance is ignoring remote download rules set in the current /// application instance; otherwise, . /// public bool IgnoreRemoteDownloadSettings { get { return this.ignoreRemoteDownloadSettings; } } /// /// Gets the Uri of the remote file being downloaded. /// public Uri Uri { get { return this.url; } } /// /// Gets or sets the length of time, in milliseconds, that a remote file download attempt can /// last before timing out. /// /// /// This value can only be set if the instance is supposed to ignore the remote download settings set /// in the current application instance. /// /// /// Set this value to 0 if there should be no timeout. /// /// /// public int TimeoutLength { get { return this.IgnoreRemoteDownloadSettings ? this.timeoutLength : TimeoutMilliseconds; } set { if (!this.IgnoreRemoteDownloadSettings) { throw new SecurityException("Timeout length can not be adjusted on remote files that are abiding by remote download rules"); } if (value < 0) { // ReSharper disable once NotResolvedInText throw new ArgumentOutOfRangeException("TimeoutLength"); } this.timeoutLength = value; } } /// /// Gets or sets the maximum download size, in bytes, that a remote file download attempt can be. /// /// /// This value can only be set if the instance is supposed to ignore the remote download settings set /// in the current application instance. /// /// /// Set this value to 0 if there should be no timeout. /// /// /// public int MaxDownloadSize { get { return this.IgnoreRemoteDownloadSettings ? this.maxDownloadSize : MaxBytes; } set { if (!this.IgnoreRemoteDownloadSettings) { throw new SecurityException("Max Download Size can not be adjusted on remote files that are abiding by remote download rules"); } if (value < 0) { // ReSharper disable once NotResolvedInText throw new ArgumentOutOfRangeException("MaxDownloadSize"); } this.timeoutLength = value; } } #endregion #region Methods #region Internal /// /// Returns the WebResponse used to download this file. /// /// /// This method is meant for outside users who need specific access to the WebResponse this class /// generates. They're responsible for disposing of it. /// /// /// /// The WebResponse used to download this file. /// /// The . /// internal async Task GetWebResponseAsync() { WebResponse response = await this.GetWebRequest().GetResponseAsync(); long contentLength = response.ContentLength; // WebResponse.ContentLength doesn't always know the value, it returns -1 in this case. if (contentLength == -1) { // Response headers may still have the Content-Length inside of it. string headerContentLength = response.Headers["Content-Length"]; if (!string.IsNullOrWhiteSpace(headerContentLength)) { contentLength = long.Parse(headerContentLength, CultureInfo.InvariantCulture); } } // We don't need to check the url here since any external urls are available only from the web.config. if ((this.MaxDownloadSize > 0) && (contentLength > this.MaxDownloadSize)) { response.Close(); throw new SecurityException("An attempt to download a remote file has been halted because the file is larger than allowed."); } return response; } /// /// Returns the remote file as a String. /// /// This returns the resulting stream as a string as passed through a StreamReader. /// /// /// The remote file as a String. internal string GetFileAsString() { Task responseTask = this.GetWebResponseAsync(); using (WebResponse response = responseTask.Result) { Stream responseStream = response.GetResponseStream(); if (responseStream != null) { // Pipe the stream to a stream reader with the required encoding format. using (StreamReader reader = new StreamReader(responseStream, Encoding.UTF8)) { return reader.ReadToEnd(); } } return string.Empty; } } #endregion #region Private /// /// Performs a check to see whether the application is able to download remote files. /// private void CheckCanDownload() { if (!this.IgnoreRemoteDownloadSettings && !AllowRemoteDownloads) { throw new SecurityException("Application is not configured to allow remote file downloads."); } } /// /// Creates the WebRequest object used internally for this RemoteFile instance. /// /// /// /// The WebRequest should not be passed outside of this instance, as it will allow tampering. Anyone /// that needs more fine control over the downloading process should probably be using the WebRequest /// class on its own. /// /// private WebRequest GetWebRequest() { // Check downloads are allowed. this.CheckCanDownload(); // Check the url is from a whitelisted location. this.CheckSafeUrlLocation(); if (this.webRequest == null) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this.Uri); request.Headers["Accept-Encoding"] = "gzip"; request.Headers["Accept-Language"] = "en-us"; request.Credentials = CredentialCache.DefaultNetworkCredentials; request.AutomaticDecompression = DecompressionMethods.GZip; if (this.TimeoutLength > 0) { request.Timeout = this.TimeoutLength; } this.webRequest = request; } return this.webRequest; } /// /// Returns a value indicating whether the current url is in a list of safe download locations. /// private void CheckSafeUrlLocation() { string upper = this.url.Host.ToUpperInvariant(); // Check for root or subdomain. bool validUrl = false; foreach (Uri uri in RemoteFileWhiteList) { if (!uri.IsAbsoluteUri) { Uri rebaseUri = new Uri("http://" + uri.ToString().TrimStart(new[] { '.', '/' })); validUrl = upper.StartsWith(rebaseUri.Host.ToUpperInvariant()) || upper.EndsWith(rebaseUri.Host.ToUpperInvariant()); } else { validUrl = upper.StartsWith(uri.Host.ToUpperInvariant()) || upper.EndsWith(uri.Host.ToUpperInvariant()); } if (validUrl) { break; } } if (!validUrl) { throw new SecurityException("Application is not configured to allow remote file downloads from this domain."); } } #endregion #endregion } }