code it

Martins Tech Blog

HTTPS-Redirect - aber konfigurierbar bitte

Manchmal möchte man erzwingen, dass komplette Seiten oder Subdomains - oder um es anders zu formulieren: ganze ASP.NET Webanwendungen - nur per HTTPS erreichbar sind. Real-World-Beispiele gibt es zuhauf: Banking-Seiten, Shops usw.

Eine Möglichkeit ist es, das Ganze fix im Code zu verdrahten. In ASP.NET gibt es ein Filter-Attribut, das genau diesen Redirect vornimmt - das RequireHttpsAttribute. Dieses kann man einfach über die Controller oder Actions schreiben oder eben für den hier betrachteten Fall als globalen Filter definieren.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        // für die ganze Seite HTTPS erzwingen
        filters.Add(new RequireHttpsAttribute());
    }
}

Nun möchte man aber vielleicht nicht immer auf die HTTPS-Seite weiterleiten, sondern nur in bestimmten Fällen. Ich würde beispielsweise gern auf meiner Entwicklungsmaschine auf SSL und den Aufwand den man dafür betreiben muss verzichten. Und ich hätte es gern konfigurierbar. Eine andere Instanz der Anwendung braucht dieses strikte Verhalten vielleicht nicht.

Dafür gibt es auch was - nämlich die web.config. Unter dem Key system.webServer können Redirects definiert werden. Ein entsprechender Eintrag könnte beispielsweise wie folgt aussehen:
<system.webServer>
  <rewrite>
    <rules>
      <rule name="HTTP to HTTPS redirect" stopProcessing="true">
        <match url="^(.*)$"/>
        <conditions>
          <add input="{HTTPS}" pattern="^OFF$" ignoreCase="true" />
          <add input="{HTTP_HOST}" matchType="Pattern" pattern="^localhost(:\d+)?$" negate="true" />
          <add input="{HTTP_HOST}" matchType="Pattern" pattern="^127\.0\.0\.1(:\d+)?$" negate="true" />
        </conditions>
        <action type="Redirect" redirectType="Permanent" url="https://{HTTP_HOST}/{R:1}" />
      </rule>
    </rules>
  </rewrite>
</system.webServer>

Was wird hier gemacht? Wie der Name schon andeutet, wird ein permantener (also Statuscode 301) Redirect vorgenommen - allerdings mit ein paar Ausnahmen: Wenn der Host localhost oder 127.0.0.1 ggf. noch gefolgt von einem Port ist, wird auf den Redirect verzichtet, denn dann befinde ich mich entweder auf dem Webserver oder auf der Entwicklungsmaschine und greife darüber zu. Eigentlich ganz einfach - oder?

Clickjacking und was man dagegen tun kann

"Unsere neue Webanwendung ist anfällig für clickjacking" - nachdem ich diese Information bekam, musste ich zugegebenermaßen erstmal ganz scharf überlegen, was man mir damit sagen wollte.

Was steckt dahinter?
Lädt man eine fremde Seite in einem iframe, so kann man darüber transparente Steuerelemente legen. Der Benutzer denkt nun, er interagiert mit der eigentlichen Anwendung, klickt aber in Wahrheit auf die transparenten Elemente und löst so Aktionen aus, die er nicht auslösen möchte. Ein eher harmloses Beispiel dafür wäre ein transparenter Like-Button für eine fremde Facebookseite der z.B. über einem Google-Suchformular liegt und so statt der Suche erst einmal den Like auslöst.

Wie begegnet man nun diesem Problem?
Es gibt mehrere Ansätze, die man verfolgen kann. Eine in meinen Augen sehr gute Zusammenfassung dafür bietet dieser Blogpost. Erste Möglichkeit ist es, JavaScript zu verwenden und so dem Browser das Laden der Seite im iframe zu verbieten. Neuere Browser (lt. Wikipedia Internet Explorer ab 8.0, Firefox ab 3.6.9, Opera ab 10.50, Safari ab 4.0 und Chrome ab 4.1.249.1042) unterstützen zudem den X-Frame-Options-Header, den man in der Antwort mitsenden kann, um so die Darstellung im iframe zu unterbinden.

Eine Möglichkeit, dies in ASP.NET-Anwendungen zu verwirklichen findet man in diesem Artikel zum Thema Security. Und wenn man einmal dabei ist, an den Headern herumzuschrauben, kann man auch gleich noch einen Blick auf die anderen Header werfen, die automatisch hinzugefügt werden.

maxRequestLength vs. maxAllowedContentLength

Wer sich mit Dateiuploads beschäftigt wird sich früher oder später Gedanken über Dateigrößenbeschränkungen machen wollen oder müssen.

Die Einstellung dafür findet sich in der web.config. Jedoch gibt es dabei gewisse Dinge zu beachten:

  1. Die MSDN ist sehr spärlich mit Informationen zu den Stellen an denen Änderungen notwendig sind und den vorhandenen Abhängigkeiten.
  2. Je nach verwendeter IIS-Version sind unterschiedliche Werte anzupassen.

Konfiguration des IIS

Relevant ist das Attribut maxAllowedContentLength im Element system.webServer / security / requestFiltering / requestLimits. Zu beachten ist hier, dass dieser Wert in Byte ist. Für Dateigrößen von 20 MB ist hier daher 20971520 einzutragen.

Konfiguration der ASP.NET Runtime

Relevant ist das Attribut maxRequestLength im Element system.web / httpRuntime. Zu beachten ist, dass dieser Wert in Kilobyte ist. Für Dateigrößen von 20 MB ist hier daher 20480 einzutragen. Wird der IIS 6 verwendet, spielt maxAllowedContentLength keine Rolle und es muss nur maxRequestLength angepasst werden.

 

In die JSON-Serialisierung eingreifen

Der Anwendungsfall

In einer Seite sollen Diagramme angezeigt werden. Für die Darstellung habe ich mich für flot entschieden. Dabei handelt es sich um eine JavaScript-Bibliothek, die in einem Platzhalter mit Hilfe von Canvas-Elementen Charts in diversen Formaten zeichnen kann. 

Im ersten Beispiel soll ein Tortendiagramm erzeugt werden. 

Die Datenstruktur ist dabei recht simpel. Es handelt sich um ein Array von Objekten mit jeweils einem Label und einem Zahlenwert für die Daten.

var pieData = [{ label: "geschieden", data: 5 }, { label: "verheiratet", data: 25 }, { label: "ledig", data: 30 }];
$.plot($("#myChart"), pieData, {
    series: {
        pie: {
            show: true
        }
    },
    grid: {
        hoverable: true
    },
    legend: {
        labelBoxBorderColor: "none"
    }
});

Im zweiten Beispiel soll ein Liniendiagramm erzeugt werden, das Werte mehrerer Serien über einen Zeitraum darstellen kann.

Die Datenstruktur ist ähnlich wie die des Tortendiagramms. Nur nun enthält data nicht mehr einen einzelnen Wert, sondern ein Array von x-y-Werten. das Beispiel beinhaltet die Daten vom 01.11. bis 03.11.2012. Die x-Achse ist auf den Zeitraum 31.10. bis 04.11. arretiert, damit der erste Datenwert nicht direkt am Rand des Charts liegt.

var lineData = [
    { label: "aktive Kunden", data: [[1351724400000, 213], [1351810800000, 215], [1351897200000, 217]] },
    { label: "Support Tickets", data: [[1351724400000, 100], [1351810800000, 80], [1351897200000, 70]] }
];
$.plot($("#myChart"), lineData, {
    xaxis: {
        mode: "time",
        min: new Date(2012, 09, 31).getTime(),
        max: new Date(2012, 10, 4).getTime(),
    },
    yaxis: {
        tickDecimals: 0,
        min: 0,
        max: 300
    },
    series: {
        lines: { show: true },
        points: {
            radius: 3,
            show: true,
            fill: true
        }
    },
    grid: {
        hoverable: true
    },
    legend: {
        labelBoxBorderColor: "none"
    }
});

So weit so gut. Mehr...

IIS Express komfortabel konfigurieren

... oder "Ich hab da mal was vorbereitet".

Wer schon einmal versucht hat, seinen IIS Express mit SSL mit einem abweichenden Hostnamen (also nicht localhost oder der tatsächliche Rechnername) laufen zu lassen wird es kennen - mit nur sieben winzigen Schritten kommt man zum Erfolg:

  1. hosts-Datei anpassen
  2. Bindings in der applicationhost.config des IIS Express anpassen
  3. mit netsh die Urls für gesicherte und ungesicherte Verbindungen in http.sys reservieren
  4. ggf. mit netsh die entsprechenden Ports an der Windows Firewall öffnen
  5. mit makecert selbstsigniertes Zertifikat erstellen
  6. in der Management-Konsole den Fingerabdruck des neuen Zertifikats suchen
  7. das erstellte Zertifikat mit netsh dem gewünschten SSL-Port zuweisen

Und da das RequireSSL-Attribut fest auf Port 443 verweist und man ggf. einen anderen Port verwendet, kann es sein, dass man noch eine URL-Rewrite-Rule in der web.config einrichten muss.

Die ganzen Schritte hat Scott Hanselmann sehr detailliert in seinem Post Working with SSL at Development Time is easier with IISExpress zusammengefasst.

Interessant ist der letzte Abschnitt des genannten Posts - denn dort zeigt er, dass es noch einen undokumentierten Weg gibt, der uns die meisten der genannten Schritte abnimmt. Mit Hilfe des Kommandozeilentools IISExpressAdminCmd können solche benutzerfreundlichen Urls in http.sys und hosts-Datei hinzugefügt und auch wieder entfernt werden - und das sowohl für ungesicherte als auch ssl-gesicherte Urls. Einzig um die Anpassung der applicationhost.config und ggf. notwendiger Url-Rewrite-Regeln kommt man auch damit nicht herum. 

Ein kleines Manko hat dieses Tool - es werden nur reine Rechnernamen akzeptiert (z.B. contoso); also die Konfiguration einer kompletten Url mit verschiedenen Levels (z.B. dev.contoso.com) ist damit nicht möglich. In diesem Fall kann man nicht die Abkürzung verwenden, sondern ist auf den langen Weg angewiesen.

Englische Version Englische Version

Request Validation an eigene Bedürfnisse anpassen

Die Validierung der an die Webanwendung übertragenen Daten spielt eine zentrale Rolle, ist sie doch der Garant dafür, dass kein schadhafter Code eingeschleust werden kann und verhindert Cross Site Scripting (XSS). Und wer hat nicht schon einmal den YOSD gesehen, der erscheint, wenn ein Wert eingegeben wird, der auch nur im entferntesten wie HTML aussieht.

Was hier passiert ist klar. Die Request Validation von ASP.NET hat in den Eingabewerten einen Eintrag gefunden, den sie als potenziell unsicher erkannt hat und schützt nun die Anwendung indem eine HttpRequestValidationException geworfen wird. Was für Entwickler an der Request Validierung störend ist, ist, dass sie an einer Stelle vorgenommen wird, an der nicht so richtig gut eingegriffen werden kann - sie geschieht vor der BeginRequest-Phase des Http Requests. In meinem Post zum Attribut AllowHtml habe ich schon gezeigt, wie man mit Hilfe des AllowHtmlAttribute an Eigenschaften des Models die Request Validierung für einzelne Feldwerte deaktivieren kann. Aber ASP.NET bietet noch eine weitere Möglichkeit, in die Validierung einzugreifen: eine eigene Implementierung der Request Validation. Mehr...

ReadOnly(true) vs. Editable(false)

Der DefaultModelBinder in ASP.NET MVC ist die Klasse, die anhand von jeder Menge Magie die im Post enthaltenen Daten in die jeweiligen Properties im Model schreibt. Wenn alle Eigenschaften öffentliche Getter und Setter haben und auch auf der Seite bearbeitbar sein sollen, dann funktioniert diese Magie auch problemlos.

Spannend wird es beispielsweise dann, wenn man Eigenschaften des Models auf der Seite zwar anzeigen, dem Benutzer aber nicht erlauben möchte diese Werte zu ändern. Nichts leichter als das, gibt es doch das ReadOnlyAttribute. Aber schon dann wenn das erste Mal ein Post vom Client zurück an den Server geschickt wird, merkt man, dass ReadOnly keine so gute Idee ist, denn der DefaultModelBinder nimmt dieses Attribut sehr ernst und mit dem ReadOnly-Attribut markierte Eigenschaften werden genauso behandelt, wie auch Eigenschaften ohne öffentlichen Setter behandelt werden würden - sie werden ignoriert.

Ursache ist die interne Methode ShouldUpdateProperty. Diese ermittelt (wie der Name schon sagt), ob es sich um eine Eigenschaft handelt, die beim Binden beachtet wird oder nicht.

Mehr...

HtmlHelper und Interfaces

MVC-Views an ein Modell zu binden ist eigentlich ganz einfach: Man definiert eine Klasse, und typisiert die View mit Hilfe von @model. Das Ganze funktioniert super mit konkreten Typen und auch mit Interfaces:

@using EmployeeApplication.Models
@model Employee

Name:  @Html.DisplayFor(m => m.LastName), @Html.DisplayFor(m => m.FirstName)
Salary: @Html.DisplayFor(m => m.Salary)

Mein Beispiel ist recht einfach umrissen: Es gibt eine Klasse Employee, die einen Mitarbeiter eines Unternehmens definiert, mit typischen Eigenschaften wie Name und Jahresgehalt. Mit Hilfe der Html-Helper-Methode DisplayFor werden die Daten des jeweiligen Mitarbeiters angezeigt.

So weit alles recht unproblematisch.

Nicht immer möchte man auf konkrete Typen binden. Aus diesem Grund ändere ich mein Modell etwas um: Es wird das neue Interface IPerson definiert. Dieses definiert die Eigenschaften einer Person - also Vorname und Nachname. Davon abgeleitet wird nun noch das Interface IEmployee definiert, das IPerson um die Eigenschaft Jahresgehalt erweitert.

Mehr...

MVC-Views kompilieren

Im Standard werden MVC-Views beim ersten Zugriff kompiliert. Das hat einen großen Nachteil: Man erfährt erst beim Navigieren zu der View im Browser (oder bei der Ausführung der Coded UI Tests), dass ein Kompilierungsfehler vorliegt. Besonders bei größeren Änderungen an der Domäne wäre es aber schon sinnvoll, schon während des Continous Integration Builds zu wissen, wo noch Probleme vorliegen.

Die Einstellung ist ganz einfach. In der *.csproj-Datei des Web-Projektes gibt es eine Eigenschaft MvcBuildViews, die man lediglich auf true setzen muss.

<MvcBuildViews>true</MvcBuildViews>

Wenn man möchte, kann man diese Einstellung auch noch abhängig von der Build-Konfiguration setzen - also z.B. nur bei Release-Builds. Wer genau wissen möchte, wie man dazu vorgeht, dem empfehle ich die Blogposts von Malcolm Sheridan, der das allgemeine Vorgehen beschreibt und K. Scott Allan, der noch auf einige Besonderheiten (z.B. bei der Verwendung des Entity Frameworks) hinweist.

Access denied due to invalid credentials - aber nur manchmal?

Heute bin ich über ein sehr seltsames Phänomen gestolpert. Gesetzt ist eine ASP.NET MVC3 Anwendung. Im Debug auf dem lokalen IISExpress läuft diese auch wunderbar, und auch auf der Testumgebung (einem Windows Server 2008) läuft die Anwendung - zumindest so lange man mit localhost darauf zugreift. Alle Zugriffe über die IP-Adresse des Servers enden mit einem 401er-Fehler - und das sowohl von anderen Rechnern aus als auch vom Server selbst.

Und das Seltsame daran: Es betrifft auch nur diese eine Anwendung - alle anderen auf diesem Server laufenden Web-Anwendungen laufen problemlos und sind auch Remote erreichbar. Nach mehreren erfolglosen Einstellungsversuchen im IIS7 - Bindings, Berechtigungen und was man sonst alles falsch machen kann, IIS-Neustarts und sogar Serverneustarts ändert sich am Problem trotzdem nichts. Der Zugriff über localhost funktioniert, aber der Zugriff über die IP-Adresse eben nicht.

Letztendlich brachte ein Blogeintrag von John Tindell die rettende Lösung - und auch wenn mir wirklich unklar ist, WARUM das funktioniert - es funktioniert.

Lösung: Fügt man in der web.config im Bereich system.webServer den folgenden Eintrag hinzu und ruft die Seite dann nochmals auf, so funktioniert der Aufruf nun auch mit der IP-Adresse:

<httpErrors errorMode="Details" />

Seltsam daran: Dieser Eintrag hat eigentlich nur etwas damit zu tun, wie das Error-Handling für die Anwendung ist und wie demzufolge Fehlermeldungen ausgegeben werden. Detailed Errors anzuschalten kann natürlich nicht die Lösung sein, da man auf extern erreichbaren Servern natürlich keine Interna des Webservers und der Applikationsstruktur preisgeben möchte. Deswegen kann das noch nicht die finale Lösung sein.