Geuploade bestanden controleren

Ingediend door Dirk Hornstra op 07-dec-2018 19:23

Je hebt online tools om bestanden te scannen op virussen. Stel dat ik dit zou willen doen, is dat mogelijk? 

Mijn eerste ingeving is dat dit zou moeten kunnen. Je kunt een class maken die erft van IHttpModule die je toevoegt in je web.config-bestand zoals hieronder:


<system.webServer>
    <modules>
      <add name="SecurityModule" type="SecurityModule.UploadedFileScanner, SecurityModule" />
    </modules>
</system.webServer>

In die module controleer je of er bestanden geüpload worden. Deze vang je dan af (toch ergens lokaal opslaan?) of in een byte-array inlezen en dan aanbieden aan de online-virusscanner. Als die een "OK" terug geeft komt het moeilijkste deel, namelijk de bestanden "alsnog" uploaden. Als je namelijk deze interceptie uitgevoerd hebt, dan is de originele collectie van HttpPostedFiles niet meer bruikbaar (context.Request.Files). Je moet "dus" een nieuwe System.Net.HttpClient aanmaken die nogmaals jouw POST uitvoert, het resultaat van die actie moet je teruggeven in het originele request en je bent klaar. Dit klinkt niet heel echt moeilijk. En als ik het met een WebForm (TestPagina.aspx) probeer, werkt het ook.

Dat was de theorie, toen kwam de praktijk en viel het toch tegen. Deze code moet namelijk werken op een omgeving met classic ASP-bestanden. Voor het afvangen van uploads gebruiken we een COM-component. En die component stierf volledig af na mijn gemanipuleerde upload. Hoe los je dit dan op? En even een spoiler, ik heb het werkend gekregen, dus al de zaken die we nu langslopen hebben er uiteindelijk voor gezorgd dat het naar behoren werkt.

Ik heb zitten switchen tussen de normale upload (even in de web.config het <add name=... deel tussen <!-- en --> zetten) en het wel actief zijn van de module. Want dan kun je de verschillen terugzien.

In de ASP-pagina waarin geüpload werd onderstaande code gebruikt:


<%

if Request.QueryString("upload") = "true" then
    a = Request.BinaryRead(Request.TotalBytes)
    response.binarywrite a
    response.end
end if
%>

Hier kwam ik al een verschil tegen. In de oude code staat: Content-Disposition: form-data; name="field1", in mijn output stond: Content-Disposition: form-data; name=field1. Dus zonder de dubbele quotes. Dit heb ik toegevoegd. 

Vervolgens heb je de headers die meegestuurd worden:


<%
if Request.QueryString("upload") = "true" then
    for each item in request.servervariables
        response.write item & " = " & request.servervariables(item) & "<HR>"
    next
end if
%>

De items in je POST worden met een unieke tekstwaarde gescheiden (in een gewone post heb je veld1=waarde1&veld2=waarde2), maar bij een upload heb je een "boundary". In de header bij de normale upload stond dit er als multipart/form-data; boundary=uniekeboundarywaarde in, maar bij mijn upload als multipart/form-data; boundary="uniekeboundarywaarde", dus met dubbel quotes er omheen. Die heb ik weggehaald.

Vervolgens heb je natuurlijk dat je niet in een "loop" moet komen. Je upload het bestand naar de site. Vervolgens upload deze module de gescande bestanden. Dat moet afgevangen worden, anders worden de bestanden nogmaals gescand, nogmaals geüpload en begint er een server te roken. Dit kun je redelijk simpel oplossen door in je "onder water request" met de HttpClient een eigen header toe te voegen. Als je die in je request hebt, dan is het bestand al gecontroleerd en hoeven we niets te doen. In mijn controle had ik echter een bug geïntroduceerd:


string filterHeaderValue = context.Request.Headers.Get(SecurityFileScanValue);

if (string.IsNullOrEmpty(filterHeaderValue)&&context.Request.Files.Count > 0)
{
.... // scan code
}

Mijn fout is dat ik ook meteen controleer of er wel bestanden mee geüpload worden. Zodra je die controle uitvoert beschouwt de COM component je upload al als ongeldig! Dus het "onder-water-request" ging altijd fout.
De controle moet dus gesplitst worden:


string filterHeaderValue = context.Request.Headers.Get(SecurityFileScanValue);

if (string.IsNullOrEmpty(filterHeaderValue))
{
    if (context.Request.Files.Count > 0)
   {
       .... // scan code
   }
}

Door deze aanpassingen kreeg ik een werkende module. Voor de volledigheid laat ik hieronder nog even zien hoe ik het "onder-water-request" opgezet heb, zo moet je bijvoorbeeld ook de cookies mee nemen, mocht je upload bijvoorbeeld achter een inlog zitten en je op basis van de cookies uitleest of iemand wel of niet ingelogd is.


HttpFileCollection files = context.Request.Files;
// .... code om af te vangen, te scannen, etc.

string actionUrl = context.Request.Url.ToString();

// to make the upload work, we must set the cookies
Uri target = new Uri(actionUrl);
HttpClientHandler httpClientHandler = new HttpClientHandler();
httpClientHandler.UseCookies = true;
httpClientHandler.CookieContainer = new CookieContainer();
foreach (string item in context.Request.Cookies.AllKeys)
{
    try
    {
        string cookieName = item;
        string cookieValue = context.Request.Cookies[item].Value;
        string cookiePath = context.Request.Cookies[item].Path;
        string cookieDomain = context.Request.Cookies[item].Domain;

        Cookie cookie = null;
        if (string.IsNullOrEmpty(cookieDomain))
        {
            if (string.IsNullOrEmpty(cookiePath))
            {
                cookie = new Cookie(cookieName, cookieValue, "/", target.Host);
            }
            else
            {
                cookie = new Cookie(cookieName, cookieValue, cookiePath, target.Host);
            }
        }
        else
        {
            cookie = new Cookie(cookieName, cookieValue, cookiePath, cookieDomain);
        }
        httpClientHandler.CookieContainer.Add(cookie);
    }
    catch (Exception)
    { /* swallow exceptions. if uploads succeeds it is no problem some cookies fail */}

}

using (var client = new HttpClient(httpClientHandler))
{
    string boundary = string.Format("----Boundary{0}", Guid.NewGuid().ToString().Replace("-", ""));
    using (var formData = new MultipartFormDataContent(boundary))
    {
        foreach (var item in context.Request.Form)
        {
            var stringContent = new StringContent(context.Request.Form[item.ToString()]);
            stringContent.Headers.ContentDisposition =
            new ContentDispositionHeaderValue("form-data")
            {
                Name = "\"" + item.ToString() + "\""
            };
            stringContent.Headers.ContentType = null;
            formData.Add(stringContent);
        }
        for (int k = 0; k < files.Count; k++)
        {
            string filename = string.Format("{0}_{1}", uniqueValue, files[k].FileName);
            string path = Path.Combine(context.Server.MapPath("/"), TemporarySaveLocation, filename);
            byte[] fileBytes = System.IO.File.ReadAllBytes(path);
            var fileContent = new ByteArrayContent(fileBytes);

            fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
            {
                Name = "\"" + context.Request.Files.AllKeys[k].ToString() + "\"",
                FileName = "\"" + files[k].FileName + "\""
            };
            fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(files[k].ContentType);
            formData.Add(fileContent);
        }
        formData.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data; boundary=" + boundary);
        formData.Headers.Add(MetaDataCleanerHeader, filterHeaderValue);

        var response = client.PostAsync(actionUrl, formData).Result;
        string html = response.Content.ReadAsStringAsync().Result;

        _InterceptedHtmlResponseCache.Add(filterHeaderValue, new InterceptedHtmlResponse() { httpStatusCode = response.StatusCode, html = html }, null, DateTime.Now.AddMinutes(2), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);

    }

}

Je ziet hier boven dat ik ook bij de normale formulier-waarden expliciet het Content-Type leeg gemaakt heb (NULL), omdat die ook in de originele post niet gezet werden, ook nog een mogelijke fix voor het uploadprobleem. 

Je ziet dat ik de response in een Cache-Object zet. Dit is omdat je nu in BeginRequest zit. Het tonen van de data zit gekoppeld aan EndRequest. Maar dat loopt asynchroon door elkaar (je upload het bestand, dat komt door deze functie, maar bijvoorbeeld de menu-pagina komt eerder in EndRequest en daar wil je natuurlijk niet deze output tonen.

Daarom ook nog even de code zoals deze in EndRequest zit:


 

HttpContext context = null;
try
{
    HttpApplication application = (HttpApplication)source;
    context = application.Context;
    string filterHeaderValue= context.Response.Headers.Get(SecurityFileScanValue);

    if (string.IsNullOrEmpty(filterHeaderValue) == false && _InterceptedHtmlResponseCache.Get(filterHeaderValue) != null)
    {
        context.Response.ClearContent();
        context.Response.Headers.Remove(SecurityFileScanValue);
        InterceptedHtmlResponse responseToShow = (InterceptedHtmlResponse)_InterceptedHtmlResponseCache.Get(filterHeaderValue);
        context.Response.StatusCode = (int)responseToShow.httpStatusCode;
        context.Response.Write(responseToShow.html);
        _InterceptedHtmlResponseCache.Remove(filterHeaderValue);
    }
}
catch (Exception) { }

En voor ik het vergeet, daarna liep ik nog tegen een probleem aan. Toen ik vervolgens 2 bestanden ging uploaden (een PDF van 7 MB en 1 van 11 MB kreeg ik een foutmelding dat er te weinig uploadcapaciteit beschikbaar was. Ik weet niet of dit al zo was, of dat het nu door dit request kwam. In ieder geval kun je dat verhogen door de volgende instellingen in de web.config toe te passen:


<system.web>
    <httpRuntime targetFramework="4.5.2" maxRequestLength="102400" />
</system.web>

<system.webServer>
  <security>
      <requestFiltering>
          <requestLimits maxAllowedContentLength="13107200" />
      </requestFiltering>
  </security>      
</system.webServer>

Mocht je een vergelijkbaar probleem hebben dan hoop ik dat je met deze code ook een werkende oplossing kunt programmeren!