Remove unwanted response headers from IIS hosted site

You may receive an e-mail from your Security team after a scan, that your Web application exposing certain Response Headers that might get exploited by attackers to gain unauthorized access to the System.
While it may not always be necessary to mitigate these kind of vulnerabilities, you still have to weigh the options. This might be true even for say, a Reactjs app hosted on IIS.

If you do need to remove certain Response Headers like Server, X-AspNet-version and X-Powered-By, these can be taken care of at the IIS Server level or in web.config file.

For the Server and X-Powered-By Response Headers, create Server variables as under URL Rewrite option as shown below:

Set the variable names as RESPONSE_SERVER and RESPONSE_X-POWERED-BY respectively.

Next create outbound rules using URL Re-write in IIS specifically for the Website. If you create these rules at the Server level, it’ll be applied to all Websites.

With Pre-condition set to None, make the below configuration using URL re-write in the Outbound rule. The same configuration requires to be done for RESPONSE_X-POWERED-BY server variable.

You can then test these Response Headers values should come as blank in the Dev Tools of your Browser.

For the X-AspNet-version Response Header, make the below entry in your Web.Config file. This will remove the Response Header altogether.

Add the following line in your web.config in the <system.Web> section:
<httpRuntime enableVersionHeader="false" />

Handle WebDriver browser prompt for ADFS credentials Selenium C#

While working on Automating the UI using Selenium C#, you might encounter a scenario where a website requires to be authenticated with ADFS. The browser would prompt the user to provide the AD domain credentials to authorize for accessing the website. So automating this scenario with the Selenium C# API, would require you to provide the domain credentials programatically.

The WebDriver used is Firefox with Geckodriver. Another post explains the working of the automation and initialization of the WebDriver in detail here.

Code sample is as shown below:

public static IWebDriver driverWeb;
driverWeb.Navigate().GoToUrl(link);
try
{
	driverWeb.SwitchTo().Alert().SendKeys("Username" + Keys.Tab + "Password");
	driverWeb.SwitchTo().Alert().Accept();
}
catch { }

try
{
	driverWeb.FindElement(By.Id("acknowledgeCookieId")).Click();
}
catch { }

Take screen-shot of webpage with Firefox and Geckodriver using Selenium with C#

Gecko is a web browser engine used in many applications developed by Mozilla Foundation and the Mozilla Corporation. It is a proxy for using W3C WebDriver-compatible clients to interact with Gecko-based browsers, in this case, Firefox. Gecko Driver acts as the bridge between your script in Selenium and the Firefox browser.

Firefox browser implements the WebDriver protocol using an executable called GeckoDriver.exe. This executable starts a server on your system through which all interaction happens. It translates calls into the Marionette automation protocol by acting as a proxy between the local and remote ends.

To generate the screenshot, the web page will be loaded in the Firefox WebDriver and the height of the window adjusted appropriately. Every GeckoDriver process spawns 4 Firefox processes which you can check through Task Manager. The Preview generation logic can be written as a Web Service and hosted on IIS. The AppPool would require to be run with LocalSystem permission to enable communication between GeckoDriver and Firefox WebDriver.

The below code will run Firefox WebDriver in headless mode:

public static IWebDriver driverWeb;
public static FirefoxOptions optionsFirefox;
static int totalWidth = 1024;
static int additionalHeight = 220;
static int additionalHeightOffset = 2180;
static int normalHeight = 768;
static int explicitWaitTime = 5;

Setup browser profile:

static Firefox()
{
	try
	{
		optionsFirefox = new FirefoxOptions();
		FirefoxProfile profile = new FirefoxProfile();
		profile.SetPreference("permissions.default.desktop-notification", 1);
		profile.AcceptUntrustedCertificates = true; //Accept SSL certificates which have expired
		profile.AssumeUntrustedCertificateIssuer = false; //Firefox assumes untrusted SSL certificates are coming from untrusted issuer 
		profile.SetPreference("general.useragent.override", Convert.ToString(ConfigurationManager.AppSettings["driverUserAgent"]));
		profile.SetPreference("layout.css.devPixelsPerPx", "0.9");
		optionsFirefox.Profile = profile;
		optionsFirefox.AddArgument("-headless");
		KillExistingProcesses();
		initializeWebDriver();
	}
	catch (Exception e)
	{
		throw e;
	}
}

Initialize Web Driver:

public static void initializeWebDriver()
{
	try
	{
		FirefoxDriverService service = FirefoxDriverService.CreateDefaultService(Convert.ToString(ConfigurationManager.AppSettings["driverPath"]));
		service.HideCommandPromptWindow = true;
		driverWeb = null;
		driverWeb = new FirefoxDriver(service, optionsFirefox, new TimeSpan(0, 0, Convert.ToInt16(ConfigurationManager.AppSettings["setDriverTimeout"])));
		driverWeb.Manage().Timeouts().ImplicitWait = new TimeSpan(0, 0, Convert.ToInt16(ConfigurationManager.AppSettings["setPageLoadTimeout"]));
		driverWeb.Manage().Timeouts().PageLoad = new TimeSpan(0, 0, Convert.ToInt16(ConfigurationManager.AppSettings["setPageLoadTimeout"]));
	}
	catch
	{
		//log exception.
	}
}

Generate Preview:

//This method will be called from a Rest Service and returns a byte array for image.
public byte[] getImage()
{
	byte[] imageDataWeb;
	try
	{
		if (driverWeb == null)
		{
			lock (padlockWeb)
			{
				//double lock check for thread-safety.
				if (driverWeb == null)
				{
					initializeWebDriver();
				}
			}
		}

		if (getHttpResponseCode(link))
		{
			lock (driverWeb)
			{
				driverWeb.Navigate().GoToUrl(link);
				Thread.Sleep(100);
				/*Start Area: Get rendered page height/width and adjust browser accordingly*/
				try
				{
					var javaScriptExecutor = driverWeb as IJavaScriptExecutor;
					var waitPage = new WebDriverWait(driverWeb, TimeSpan.FromSeconds(explicitWaitTime));
					bool readyCondition(IWebDriver webDriver) => (bool)javaScriptExecutor.ExecuteScript("return document.readyState == 'complete'");
					waitPage.Until(readyCondition);

					string HeightScript = "return document.body.scrollHeight";
					long totalHeight1 = (long)javaScriptExecutor.ExecuteScript(HeightScript);
					int totalHeight = (int)totalHeight1;
					if (totalHeight == 0)
					{
						totalHeight = normalHeight + additionalHeight;
					}
					else
					{
						if (totalHeight <= additionalHeightOffset)
						{
							totalHeight += additionalHeight;
						}
					}
					driverWeb.Manage().Window.Size = new Size(totalWidth, totalHeight);
				}
				catch (Exception e)
				{
					_txtSource.TraceEvent(TraceEventType.Error, 0, e.Message);
					_txtSource.TraceEvent(TraceEventType.Error, 0, e.StackTrace);
					_txtSource.Flush();

					driverWeb.Manage().Window.Size = new Size(totalWidth, normalHeight + additionalHeight);
				}
				/*End Area: Get rendered page height/width and adjust browser accordingly*/

				screenshot = ((ITakesScreenshot)driverWeb).GetScreenshot().AsByteArray;

				if (screenshot.Length == 0)
				{
					throw new Exception("Website not responding.");
				}

				driverWeb.Navigate().GoToUrl("about:blank");
				driverWeb.Manage().Window.Size = new Size(totalWidth, normalHeight);
				return screenshot;
			}
		}
		else
		{
			imageDataWeb = returnErrorImage(); //return error image.
		}
		return imageDataWeb;

	}
	catch (Exception e)
	{
		//log exception
		imageDataWeb = doExtractEmptyPage();
		return imageDataWeb;
	}
	finally
	{
		imageDataWeb = null;
	}
}

Create Virtual Directory in IIS

Create the Website under IIS as shown below on port 80 so it can be accessed using http and give the appropriate hostname as required. The hostname should be available in the DNS records to access over Internet/Intranet.

Create the virtual directory as shown below. Use the default Pass-through authentication for demo purpose. You can also use Physical Path credentials on your folder if required and manage using Connect as button.

The below image shows how the Virtual Directory will look like under your website in IIS. So if you have a file saved under the Attachments folder under C:\Data\Attachments\doc1.docx, the URL to access the document will be http://mywebsite/attachments/doc1.docx. The folder can also be created on another physical drive on the same Server or can also be accessed using a UNC path.

Create Virtual directory in Visual Studio 2017 IIS Express

You may be facing a scenario where the Images folder of your Project is hosted in another directory on your machine e.g. C:\Data\Images and not under the Project directory structure. In this case, if you need to check whether the Images are loading correctly, you’ll need to create a Virtual directory in your Web application/MVC/WebAPI Project.

Follow the below steps:

1. Right-click on your Project properties and go to the Web tab and provide the URL for the required Virtual directory and click on the button Create Virtual directory. This option edits the ApplicationHost.config file.

2. Run the application and right-click on the IIS Express icon in the status-bar notification tray. Select your website and open the config file.

3. Look for the site tag and find your website and you’ll find the Virtual directory nested tag. Edit the path and physicalPath attribute to the folder on your machine as shown below:

<site name="MyApi" id="2">                 <application path="/" applicationPool="Clr4IntegratedAppPool">                     <virtualDirectory path="/" physicalPath="C:\MyApi" />                 </application>                 <application path="/uploads" applicationPool="Clr4IntegratedAppPool">                     <virtualDirectory path="/" physicalPath="C:\Data\Images\uploads" />                 </application>                 <bindings>                     <binding protocol="http" bindingInformation="*:62216:localhost" />                 </bindings>             </site>

After making these changes, refresh the app to view the images in your app via localhost.

To know how to create a Virtual Directory in IIS for a website, check this post.

Show some love for the pit in my PayPal account.

Enable PUT and DELETE Http verbs for WebAPI with Cors on IIS 8.5

WebAPI in .Net by default allows only the GET, POST, OPTIONS and HEAD verbs. To allow PUT and DELETE, you need to remove the WebDAV handler and module from the request pipeline by making the following changes to the Web.Config.

The WebDAV element contains the settings that configure Web Distributed Authoring and Versioning (WebDAV) for Internet Information Services (IIS) 7 or above. WebDAV is an Internet-based open standard that enables editing Web sites over HTTP and HTTPS connections. You may get a 405 error code due to WebDAV.

<system.webServer>
    <handlers>
      <remove name="WebDAV" />
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <remove name="OPTIONSVerbHandler" />
      <remove name="TRACEVerbHandler" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
    <validation validateIntegratedModeConfiguration="false" />
    <modules>
      <remove name="WebDAVModule" />
    </modules>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="http://somedomain" />
        <add name="Access-Control-Expose-Headers" value="Content-Type, Accept, expiry, uid, access-token, token-type" />
        <add name="Access-Control-Allow-Methods" value="POST,GET,OPTIONS,PUT,DELETE" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>

Also, the ExtensionlessUrlHandler-Integrated-4.0 handler needs to allow all verbs with a * or a manual entry for specific verbs. The minimum runtime requirement is 4.0 to make this work.

The last bit to check for a message that says e.g. PUT /Rejected-By-UrlScan . Check the logs under C:\Windows\System32\inetsrv\urlscan\Logs on the Server. You may need to edit the config file to allow those verbs or remove the UrlScan 3.1 service in the ISAPI tab of your Service in IIS which points to urlscan.dll. This is usually mentioned last in the list.

CustomHeaders are part of enabling Cors. Please note that Access-Control-Allow-Origin should not allow “*” . It should mentions specific domain that requires to be allowed for Cors. You can expose custom headers using Access-Control-Expose-Headers and allow specific methods using Access-Control-Allow-Methods.

Show some love for the pit in my PayPal account.

Resize image preserving aspect ratio in C#

The aspect ratio of an image describes the proportional relationship between its width and its height. For an x:y aspect ratio, if the width is divided into x units of equal length and the height is measured using this same length unit, the height will be measured to be y units.

The below method saves images with different encoder formats of jpeg, png and gif images. You can modify the code for other available formats in C#.

public class ImageUploader
    {

public float Save(Bitmap image, int maxWidth, int maxHeight, int quality, string fileName, string ext)
        {
            // Get the image's original width and height
            int originalWidth = image.Width;
            int originalHeight = image.Height;

            // To preserve the aspect ratio
            float ratioX = (float)maxWidth / (float)originalWidth;
            float ratioY = (float)maxHeight / (float)originalHeight;
            float ratio = Math.Min(ratioX, ratioY);

            float sourceRatio = (float)originalWidth / originalHeight;

            // New width and height based on aspect ratio
            int newWidth = (int)(originalWidth * ratio);
            int newHeight = (int)(originalHeight * ratio);

            // Convert other formats (including CMYK) to RGB.
            Bitmap newImage = new Bitmap(newWidth, newHeight, PixelFormat.Format24bppRgb);

            // Draws the image in the specified size with quality mode set to HighQuality
            using (Graphics graphics = Graphics.FromImage(newImage))
            {
                graphics.CompositingQuality = CompositingQuality.HighQuality;
                graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
                graphics.SmoothingMode = SmoothingMode.HighQuality;
                graphics.DrawImage(image, 0, 0, newWidth, newHeight);
            }

            ImageCodecInfo imageCodecInfo = null;
            // Get an ImageCodecInfo object that represents the JPEG codec.
            if (ext == ".jpg" || ext == ".jpeg")
            {
                imageCodecInfo = this.GetEncoderInfo(ImageFormat.Jpeg);
            }
            else if (ext == ".png")
            {
                imageCodecInfo = this.GetEncoderInfo(ImageFormat.Png);
            }
            else if (ext == ".gif")
            {
                imageCodecInfo = this.GetEncoderInfo(ImageFormat.Gif);
            }

            // Create an Encoder object for the Quality parameter.
            Encoder encoder = Encoder.Quality;

            // Create an EncoderParameters object. 
            EncoderParameters encoderParameters = new EncoderParameters(1);

            // Save the image as a JPEG file with quality level.
            EncoderParameter encoderParameter = new EncoderParameter(encoder, quality);
            encoderParameters.Param[0] = encoderParameter;
            newImage.Save(uploadsdir + fileName, imageCodecInfo, encoderParameters);

            return sourceRatio;
        }
}
private ImageCodecInfo GetEncoderInfo(ImageFormat format)
        {
            return ImageCodecInfo.GetImageDecoders().SingleOrDefault(c => c.FormatID == format.Guid);
        }

Call the code above with the following parameters as below, keeping the following points in mind:

  • SaveImages method below is created keeping in mind, it is written for a WebAPI and the image may be received in the byte array format.
  • Use a very large height value, so the new image is re-sized restricted to new width size for thumb, medium and large sizes based on the aspect ratio.
  • The Quality category specifies the level of compression for an image. When used to construct an EncoderParameter, the range of useful values for the quality category is from 0 to 100. The lower the number specified, the higher the compression and therefore the lower the quality of the image. Zero would give you the lowest quality image and 100 the highest.
public string SaveImages(byte[] imgArr, string filename, string extension, string uploadtype, out float ratio)
        {
            const int THUMB_WIDTH = 120;
            const int MEDIUM_WIDTH = 400;
            const int LARGE_WIDTH = 900;
            const int IMG_HEIGHT = 100000;
            const int IMG_QUALITY = 100;
            ratio = 0.0f;
            string[] extensionwhitelist = new string[] { ".jpg", ".jpeg", ".gif", ".png" };

            ImageUploader uploader = new ImageUploader(uploadtype);
            
            var encfilename = encryptfilename(filename);
            try
            {
                if (!extensionwhitelist.Contains(extension))
                {
                    throw new Exception("Invalid File format.");
                }
                
                TypeConverter tc = TypeDescriptor.GetConverter(typeof(Bitmap));
                Bitmap bitmapImg = (Bitmap)tc.ConvertFrom(imgArr);

                uploader.SaveOriginal(bitmapImg, IMG_QUALITY, encfilename + extension, extension);
                uploader.Save(bitmapImg, THUMB_WIDTH, IMG_HEIGHT, IMG_QUALITY, "thumb_" + encfilename + extension, extension);
                uploader.Save(bitmapImg, MEDIUM_WIDTH, IMG_HEIGHT, IMG_QUALITY, "medium_" + encfilename + extension, extension);
                ratio = uploader.Save(bitmapImg, LARGE_WIDTH, IMG_HEIGHT, IMG_QUALITY, "large_" + encfilename + extension, extension);
            }
            catch(Exception ex)
            {
                throw new BusinessServiceException("SaveImages", "Failed saving images.", ex);
            }

            return encfilename;
        }

The method encryptfilename can be written using a method of choice for encrypting the filename of the image.