Deny file access but server properly in IIS - security

I have a js file that includes in the master page.
I want to deny the file access when user type the direct link in the browser address bar.
I've tried the URL filtering IIS,like:
<security>
<requestFiltering>
<denyUrlSequences>
<add sequence="Scripts/Foo/bar.min.js" />
</denyUrlSequences>
</requestFiltering>
</security>
This does work, when i type 'localhost://blah/Scripts/Foo/bar.min.js' I get blocked.
but the page whitch need this js file can not render.
Does anyone have a workaround? Thanks in advance!

I finnaly found this.
and added these sections to web.config:
<security>
<requestFiltering>
<filteringRules>
<filteringRule name="protectjs" scanUrl="true" scanQueryString="true">
<scanHeaders>
<clear />
<add requestHeader="Accept" />
</scanHeaders>
<appliesTo>
<clear />
<add fileExtension=".js" />
</appliesTo>
<denyStrings>
<clear />
<add string="text/html" />
</denyStrings>
</filteringRule>
</filteringRules>
</requestFiltering>
</security>
It works at present, though I know this way is not that reliable.

Not 100% sure but I don’t think there is a way to do this. When browser renders your page it actually sends the same request to the server as the one you do when you manually type in JS file in the browser.
IIS doesn’t have means to distinguish requests you make and the one browser makes in the background while processing your page.
One thing that might work is
adding some kind of ID to your JS file like this Scripts/Foo/bar.min.js?ID=E3CF305B-4444-E011-8FD2-1CC1DEE89A7F
ID is time limited and works only 20 seconds or so after it’s created (enough time for browser to load the page)
creating custom handler that will parse ID and decide if server should return the request or not
So when someone types Scripts/Foo/bar.min.js handler will reject the request but it will also reject request when expired ID is used.
However this is most probably going to be difficult to maintain and also performance intensive for your server.
If you want to hide your JS file why not obfuscate it.

Use HttpModule and Check for HttpContext.Current.Request.UrlReferrer. it will always be null for direct access to the .js file.
public class JSAccessModule : IHttpModule
{
public void Init(System.Web.HttpApplication Appl)
{
Appl.BeginRequest += new System.EventHandler(Rewrite_BeginRequest);
}
public void Rewrite_BeginRequest(object sender, System.EventArgs args)
{
//Block if direct Url is accessed
if (HttpContext.Current.Request.UrlReferrer == null)
{
HttpApplication App = (HttpApplication)sender;
string path = App.Request.Path;
string strExt = System.IO.Path.GetExtension(path);
if (strExt == ".js")
{
HttpContext.Current.Response.Redirect("~/AccessDenied.html");
}
}
}
public void Dispose() { }
}

Related

Error 405 - Method not allowed with IIS Express 10 with CORS for Web API

I know this question has been asked plenty of times, each with similar answers, but after hours on this problem, I've yet to get it resolved, so I'm hoping additional suggestions may be provided.
I'm getting Error 405 - Method not allowed
I've removed the WebDAV entries from the module and handler section as suggested.
I've also changed the ExtensionlessUrlHandler-Integrated-4.0. Removed it first as suggested but didn't work so re-added it but with a slightly different definition <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*."
verb="GET,POST,OPTIONS,PUT,DELETE"
type="System.Web.Handlers.TransferRequestHandler"
preCondition="integratedMode,runtimeVersionv4.0" />
where each verb is defined rather than using *
I've ensure CORS was enabled i.e. app.UseCors(CorsOptions.AllowAll); is called from my Startup class in public void Configuration(IAppBuilder app)
Access-Control-Allow-Methods has been set in my web.config
The weird thing is that it works just fine for DELETE but not for PUT.
Here's my System.WebServer section from my web.config:
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules runAllManagedModulesForAllRequests="true">
<remove name="WebDAVModule"/>
</modules>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Methods"
value="GET,POST,OPTIONS,PUT,DELETE" />
</customHeaders>
</httpProtocol>
<handlers>
<clear/>
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<remove name="OPTIONSVerbHandler" />
<remove name="TRACEVerbHandler" />
<remove name="WebDAV" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*."
verb="GET,POST,OPTIONS,PUT,DELETE"
type="System.Web.Handlers.TransferRequestHandler"
preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
</system.webServer>
When I run Fiddler, I'm getting the following:
HTTP/1.1 405 Method Not Allowed
Allow: GET,POST
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/10.0
X-SourceFiles: =?UTF-8?B?RDpcU3BpbmRldlxXb3JrXEpvaWZmTGlzdGluZ05lnM=?=
X-Powered-By: ASP.NET
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,OPTIONS,PUT,DELETE
Date: Mon, 15 Jul 2019 23:41:32 GMT
Content-Length: 79
{
"message": "The requested resource does not support http method 'PUT'."
}
As you can see, the Access-Control-Allow-Origin and Access-Control-Allow-Methods appears to be set correctly but the Allow: is still set to GET,POST. Why is that? Where am I suppose to change this to have all the verbs?
And finally my action in my web controller is defined as follows:
[HttpPut]
[Route("id:{Guid}")]
public async Task<IHttpActionResult> UpdateCompany(Guid id)
{
}
Pretty standard stuff!
Any ideas and/or suggestions? Remember that I'm concentrating on getting this to work on IIS Express. Once I've got that resolved, I'll check it out in IIS but I really want to get to the bottom of this first.
Any help much appreciated.
Thanks.
UPDATE-1
I've just found an article from Microsoft regarding CORS, and even thought I'm enabling it as mentioned above, I've noticed that I don't have any references in my list of references to Microsoft.AspNet.WebApi.Cors which is odd and when I try to add the [EnableCors...] attribute, no references are shown which would indicate even more clearly that it may not be installed properly or at all.
I'll check that tomorrow and update.
I've also forgot to mention that OWIN is installed and set up. In the event this may give more clues as to why I still can't resolve this problem.
UPDATE-2
My add company (POST) is defined as follows:
[HttpPost]
public async Task<IHttpActionResult> AddCompany (
CompanyRequestDto companyRequestDto)
{
}
My update company (PUT) is defined as follows:
[HttpPut]
public async Task<IHttpActionResult> UpdateCompany (
Guid Id,
CompanyRequestDto companyRequestDto)
{
}
and my WebApiConfig.cs has the following route defined in it:
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
I've removed [Route("id:{Guid}")] as I thought it could have been that, but it's not. Same issue.
As Andrei Dragotoniu suggested, commenting out my UpdateCompany function generated the very same error which means another function is being hit but I have no idea which one as none of the breakpoints I've put are being hit, nor, any of them are defined as PUT so it's confusion. I'm sure we'll get to the bottom of it.
UPDATE-3
I feel really stupid right now!! After wasting so much time researching this problem, it actually wasn't there! Unlike a POST request where you only post the object, the PUT request expected a CompanyId as part of the query string which I had omitted and cause the problem!
http://localhost:12345/Companies
instead of
http://localhost:12345/Companies/61770BAA-78A6-E911-AEB1-001A7DDA7111
Anyway, I'm glad I'm up and running and I hope nobody else will do something as silly as this but if you do, hopefully, this will help!
Do not do this on IIS Express, that's pointless. Get it working in proper IIS instead.
One thing to check, the error method tells you that the particular method you're accessing does not support PUT. This doesn't mean that the PUT verb is not enabled in config. What it means is that the particular method you're accessing does not support it.
You need to check and see which endpoint is being hit because it doesn't seem to be the one you think. Check your rules basically. Remember they get applied in order so you really want your most concrete ones to be loaded first and the most general one at the end.
One quick way to check this is to comment out the UpdateCompany(Guid id) endpoint and see if you still get the same response when you repeat the call in Postman. If you do, then it's obvious that your request is being handled by a different endpoint, not the one you think.

502 error upload csv file to web api

I have an Angular5 user interface with a file upload function. The user clicks a button and selects a file and the file is sent to the web api (asp.NET Core) method for processing.
This works fine with smaller files, but with larger files the request times out with a 502 error.
I can see the request always timesout at 120 seconds. (NOTE: I am hosting via node in development and via IIS in production).
In the case of large files I need to extend this timeout to a larger value. I've tried to achieve this in a number of ways:
Request Header - Timeout of request in angular code. I used the following code to try to set the timeout header value but it doesn't effect the 120 seconds:
export class AuthTokenInterceptor implements HttpInterceptor {
constructor(private authContext: AuthContext) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const authHeaderValue = this.authContext.getAuthenticationHeaderValue(req.url);
if (authHeaderValue) {
const authReq = req.clone({ headers:
req.headers.set('Authorization', authHeaderValue)
.set('Timeout', '100000000000000000') });
return next.handle(authReq);
}
return next.handle(req);
} }
web.config - I've tried setting the httpRuntime timeout value in the web.config file to the following (but still times out at 120 seconds):
Within IIS - I've tried setting the configuration of the "Limits" property in IIS and again, still times out at 120 seconds (and this has no relevance when I'm running through node server).
Has anyone been able to modify this 120 seconds in their Angular(2+) app requests?
Thanks for any pointers in advance!
NOTE: just for completeness, here's my asp.net core, controller method for uploading:
[HttpPost("Upload")]
public async Task<IActionResult> UploadAsync(IFormFile file)
{
// Ensure the file has contents before processing.
if (file == null || file.Length == 0)
throw new ApiException("Csv file should not be null", HttpStatusCode.BadRequest)
.AddApiExceptionResponseDetails(ErrorTypeCode.ValidationError, ErrorCode.BelowMinimumLength, SOURCE);
// Ensure the file is not over the allowed limit.
if (file.Length > (_settings.MaxCsvFileSize * 1024))
throw new ApiException("Max file size exceeded, limit of " + _settings.MaxCsvFileSize + "mb", HttpStatusCode.BadRequest)
.AddApiExceptionResponseDetails(ErrorTypeCode.ValidationError, ErrorCode.ExceedsMaximumLength, SOURCE);
// Ensure the file type is csv and content type is correct for the file.
if (Path.GetExtension(file.FileName) != ".csv" ||
!Constants.CsvAcceptedContentType.Contains(file.ContentType.ToLower(CultureInfo.InvariantCulture)))
throw new ApiException("Csv content only accepted").AddApiExceptionResponseDetails(ErrorTypeCode.ValidationError, ErrorCode.Invalid, SOURCE);
// Read csv content.
var content = await file.ReadCsvAsync<OrderCsvResponseDto>() as CsvProcessedResponseDto<OrderCsvResponseDto>;
await ProcessBulkUpload(content);
// Return information about the csv file.
return Ok(content);
}
Note - when I run the web api via IIS Express then it times out, I've run it using the command host and it doesn't time out - seem's like this may be related to an IIS setting of sorts. The web api doesn't have a web.config file due to the new version of ASP.net Core I'm using but this piece of code doesn't seem to have any bearing on IIS Express when I run through it:
var host = new WebHostBuilder()
.UseStartup<Startup>()
.UseKestrel(o => {
o.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10);
o.ShutdownTimeout = TimeSpan.FromMinutes(10);
o.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(10);
})
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseApplicationInsights()
.Build();
I will post this here in case anyone else comes across the same problem I was having. It turns out that running in IIS Express (or hosted IIS) that the config setting overrides whatever you have in code (maybe this is because the newer version of .net Core doesn't have a web.config file in the project - I'm not sure).
Anyway, I worked around this problem by carrying out the following steps:
Open IIS Express in your taskbar
Click on the app you are running (and wish to extend the request timeout for).
Clicking the app shows the config file of the application. Double click to open the config file.
Apply the following setting to the aspNetCore section:
requestTimeout="00:20:00"
Example:
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" forwardWindowsAuthToken="false" requestTimeout="00:20:00" stdoutLogEnabled="false" />
<httpCompression>
<dynamicCompression>
<add mimeType="text/event-stream" enabled="false" />
</dynamicCompression>
</httpCompression>
</system.webServer>
And that's it!
NOTE: I am hosting the app in PaaS ASE - so cant configure IIS directly here. My solution for this was now to add a web.config file to the api project, and apply my setting within it. The build process honours the web.config you've got already instead of generating one on the fly and it will keep the changes needed. In my case, the web.config looked like this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" forwardWindowsAuthToken="false" requestTimeout="00:20:00" stdoutLogEnabled="false" />
</system.webServer>
</configuration>
Hopefully this helps others!

ELMAH - Get SMTP credentials from Azure Application Settings

I've got an Azure Web App using ELMAH to log unhandled exceptions.
When I first deployed it, the web.config had the full SMTP setup defined in it, and ELMAH emailed exceptions:
<system.net>
<mailSettings>
<smtp from="me#mydomain.com">
<network host="smtp.mailprovider.com"
port="123"
userName="myUserName"
password="p#ssw0rd" />
</stmp>
</mailSettings>
</system.net>
The username and password have since been removed from the web.config, and they're now stored as application settings, configured through the Azure Portal.
Most of the emails I send still work fine, as the email code can access these application settings and use them when instantiating the SmtpClient, e.g.:
var userName = WebConfigurationManager.AppSettings["smtp.userName"];
var password = WebConfigurationManager.AppSettings["smtp.password"];
var credentials = new NetworkCredential(userName, password);
using (var smtpClient = new SmtpClient { Credentials = credentials })
{
await smtpClient.SendMailAsync(mailMessage);
}
What's the best way to get ELMAH to use the credentials stored in the application settings?
Options I can see:
There is a page on the wiki explaining how to use ELMAH's ErrorTweetModule to do an HTTP form post with the error details to any URL. The controller receiving the post could then use the stored credentials to email the details on.
The WebBase has a link to an article suggesting you can send emails directly to the recipient's SMTP server without authentication, but it says this may not work if you have DomainKeys set up, which I do.
This answer links to an article about intercepting the Mailing event, to customise the message.
I ended up creating a custom version of Elmah's ErrorMailModule, derived from the standard one, but overriding the SendMail method, based on some advice from Atif Aziz in a discussion on Google Groups.
The only changes required were to create the new module, and switch the Web.Config to use the custom module instead of the standard one.
Module
using System;
using System.Net.Mail;
namespace Test
{
public class ErrorMailModule : Elmah.ErrorMailModule
{
protected override void SendMail(MailMessage mail)
{
if (mail == null) throw new ArgumentNullException(nameof(mail));
// do what you want with the mail
// (in my case this fires up the email service, which
// gets the credentials from the Azure settings)
}
}
}
Web Config Changes
All that's required is to change the two occurrences of Elmah.ErrorLogModule, Elmah to your own module, in this case Test.ErrorMailModule.
So, instead of this...
<system.web>
<httpModules>
<add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
</httpModules>
</system.web>
<system.webServer>
<modules>
<add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
</modules>
</system.webServer>
...you should now have this:
<system.web>
<httpModules>
<add name="ErrorMail" type="Test.ErrorMailModule" />
</httpModules>
</system.web>
<system.webServer>
<modules>
<add name="ErrorMail" type="Test.ErrorMailModule" preCondition="managedHandler" />
</modules>
</system.webServer>
You will still need the errorMail section, as Elmah is still responsible for creating the email. Mine looks like this:
<elmah>
<errorMail from="user#domain.com" to="user#domain.com" subject="Custom Email Module"/>
</elmah>
Creating a HTTP request could work, but that should be the solution if everything else doesn't work IMO. Intercepting the Mailing event doesn't work, since you do not have access to the SmtpClient with the credentials in that event.
I've looked at different ways to update the SMTP settings from code. At first I though that I could just get a reference to the smtp section and update the properties, since they all have setter. But the code throw a configuration exception on runtime.
From what I can find, the only way to update the username and password in smtp section, is to read the web.config, update it and write the new version. Here's an example of writing updates to web.config:
var configuration = WebConfigurationManager.OpenWebConfiguration("~");
var section = configuration.GetSection("system.net/mailSettings/smtp") as SmtpSection;
section.Network.UserName = ConfigurationManager.AppSettings["myusername"];
section.Network.Password = ConfigurationManager.AppSettings["mypassword"];
configuration.Save();
The code actually updates the web.config. The code can be run at startup, but that would modify your web.config file locally as well. Another approach would be to run the code as part of a post deployment task with Azure.

Azure Websites Dropping Custom Headers

An Azure Website I am working on inspects custom headers of incoming requests to decide what to do with the request internally. The request is being sent/received by the website server with the customer headers:
X-HEADER-1: ...
X-HEADER-2: ...
among other standard and non-standard headers.
I verified this by inspecting the FREB logs and looking at GENERAL_REQUEST_HEADERS, which correctly includes my custom headers.
When the application receives the request, those custom headers are not there. I explicitly check for one of them then throw and dump all available headers in the error message.
I have read around that Application Request Routing module can drop these headers. I tried adding this to the website's web.config but still doesn't work:
<system.webServer>
<rewrite>
<allowedServerVariables>
<add name="HTTP_X_HEADER_1" />
<add name="HTTP_X_HEADER_2" />
</allowedServerVariables>
</rewrite>
</system.webServer>
Any idea how I can whitelist my headers to let ARR/Azure let them through?
Update 1
Here is some more info.
This works locally on my dev box. I set up the site in IIS and point it to the project folder and headers are coming in and processed as expected.
It is an ASP.NET MVC website.
Here is the part of the code that reads the header. Again, this works locally.
public class BaseController : Controller
{
public AppControllerBase(...)
{
}
protected override void Initialize(RequestContext requestContext)
{
var header1Value = requestContext.HttpContext.Request.Headers["X-HEADER-1"];
if (string.IsNullOrEmpty(header1Value))
{
var stringBuilder = new StringBuilder();
// append all headers to stringBuilder
var errorMessage = string.Format("SiteId header is not set. Headers: {0}", stringBuilder);
throw new HttpRequestException(errorMessage);
}
base.Initialize(requestContext);
}
...
}
Update 2
I just deployed the same app as an azure cloud service and it worked well. The headers were received and the app read them successfully. Something with web apps is not letting those headers through.
The answer that worked for me was in the comments. Credit goes to #Tarek Ayna.
The custom headers are transmitted when you set X-LiveUpgrade to 0.
For example:
<httpProtocol>
<customHeaders>
<add name="X-LiveUpgrade" value="0" />**
<!-- Prevent iframes -->
<add name="X-Frame-Options" value="SAMEORIGIN" />
<add name="X-XSS-Protection" value="1" />
</customHeaders>
</httpProtocol>
One possibility is to disable ARR if your services are stateless... to do that:
(Inside web.config)
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Arr-Disable-Session-Affinity" value="True" />
</customHeaders>
</httpProtocol>
</system.webServer>

URL Rewrite HTTP to HTTPS but now backwards

I have the following UrlRewrite code to change from http to https in the url but now I need it to change from https back to http.
For example when I redirect back from the Account/Logon page to the homepage.
Here are my rewriting rules:
<rewrite>
<rules>
<rule name="RequiresHTTPS-Redirect" stopProcessing="true">
<match url="(.+)" />
<conditions>
<add input="{HTTPS}" pattern="off" />
<add input="{RequiresHTTPS:{R:1}}" pattern="(.+)" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{C:1}"
appendQueryString="true" redirectType="Found" />
</rule>
</rules>
<rewriteMaps>
<rewriteMap name="RequiresHTTPS">
<add key="Account/LogOn" value="Account/LogOn" />
</rewriteMap>
</rewriteMaps>
</rewrite>
Put the logic to redirect back to the non-https page in the login page itself. The problem with redirecting from https to http is that the browser is still going to initiate an ssl connection first to the https url before it can get the redirect, so it's somewhat pointless.
There's a couple of scenarios here:
1. All content is protected by web forms <authorization>:
Your users are hitting the logon page because they've browsed to a part of the site that is protected using the <authorization> element. If this is the case you'll have a return URL passed to you in the query string: ReturnUrl. You can redirect users back to whence they came without SSL using:
return Redirect("http://" + Request.Url.Host + returnUrl);
2. Users have to logon to enable additional features:
Your users are clicking on the Logon link to enable some extra functionality that is being trimmed out on your pages if they aren't logged in. For example being able to post forum messages or view premium content.
In this case you could track where they've been before landing on the logon page. This example is based on the template application that you get with Visual Studio 2010 when you create a new MVC3 application (which you may have used as a template for your project).
In that sample application every page uses a master page Site.Master. Site.Master does a Html.RenderPartial("LogOnUserControl") to render the logon link on every page. Open the LogOnUserControl.ascx and change the code that renders the logon ActionLink to:
else
{
if(!Request.RawUrl.Contains("/Account/LogOn"))
{
Session["WhereWasI"] = Request.Url.AbsoluteUri;
}
%>
[ <%: Html.ActionLink("Log On", "LogOn", "Account") %> ]
<%
}
What we're basically doing to tracking the page the user is on if they aren't logged in. Because the Logon link is also rendered on the logon page itself we need to exclude that, hence the if statement:
if(!Request.RawUrl.Contains("/Account/LogOn"))
Then in your AccountController.cs Logon postback action method you can return the user to where they were on the site but using http insteadof https::
I've also included the redirect to non-SSL should there be a returnUrl supplied by ASP.NET Forms Authentication:
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
if (Url.IsLocalUrl(returnUrl))
{
//
// 1. All content is protected by web forms `<authorization>`:
// If there was a return URL then go back there
//
if(!String.IsNullOrWhiteSpace(returnUrl))
{
return Redirect("http://" + Request.Url.Host + returnUrl);
}
}
else
{
//
// 2. Users have to logon to enable additional features:
//
if (Session["WhereWasI"] != null)
{
return Redirect(
Session["WhereWasI"].ToString().Replace("https", "http"));
}
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("",
"The user name or password provided is incorrect.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
The example is maybe a bit simplistic but you should be able to get the general idea.

Resources