Jak se vyhnout Cross-Site přístupu při tvorbě mash-upů v ASP.NET

27. května 2008

Důležitost zabezpečení javascriptových aplikací podtrhuje fenomén mash-ups a využívání AJAXu. A to dokonce tak, že do aplikací zanášejí nová bezpečnostní rizika. V tomto článku si ukážeme, jak vytvořit proxy pro všechny zdroje z cizí domény a mít tak pod kontrolou problém Cross-Site přístupu.

Bezpečnostní problémy mash-upů

Především, i když aplikace má pouze část běžící v prohlížeči, probíhá komunikace od jedné části klientského, javascriptového kódu ke druhé. V prohlížeči ovšem sdílejí jeden datový prostor počínaje objektem window, data jsou tak vystavena všem skriptům pro čtení i zápis.

Výměna dat probíhá buď AJAXovými voláními nebo načtením <script> elementu. V prvním případě se na volání aplikuje bezpečnostní pravidlo stejného původu (same origin policy), tedy stránka se může dotazovat jen na server, ze kterého byla načtena. Pokud by toto pravidlo nebylo uplatněno (například je klient má v  prohlížeči vypnuto), otvírá se zde cesta pro útok zvaný Cross Site Request Forgery (server nerozpozná, zda je o skutečnou aktivitu uživatele, nebo její simulaci).

Toto omezení se ovšem nijak neklade na běžné načítání skriptů tagem <script>, čili toto lze jednoduše využít k obcházení bezpečnostních omezení. Komunikace může probíhat ve formátu JSON (Javascript Object Notation), takový obsah je v zásadě „bezpečně“ možné stahovat i přes <script>. Pokud však zde útočník podvrhne např. konstruktor objektu (Array ap.), Same Origin Policy se i tady stane bezzubou, útočník může dělat zásadní modifikace dat objektů stránky.

Zdá se tedy, že řešením je využívat v aplikacích čistě AJAXová volání (XmlHttpRequest) a Same Origin Policy dodržet tím, že obsah třetí strany doručíme přes proxy jako obsah z naší domény. Nemusíme pak mít ani obavu provozovat mash-up v naší aplikaci při přístupu přes HTTPS, kde je kombinace obsahu z více domén prohlížečem detekována jako bezpečnostní riziko a minimálně by tak mohla uživatele zneklidňovat varovnou hláškou nebo dokonce odepřít funkci.

Vytvoření proxy – „přeposílátka“

Proxy, tedy takové „přeposílátko“ obsahu ze serveru třetí strany, nejlépe vytvoříme jako Http Handler – protože pak můžeme jednoduše řídit, které URL je povoleno „přeposílat“ a které naopak odmítnout nebo nabídnout ke stažení přímo z vlastního serveru.

Do konfiguračního souboru si připravíme některé důležité volitelné hodnoty:

<appSettings>
  <add key=“MapScriptProxyPath“ value=“http://Provider.CZ/“ />
  <add key=“MapClientHost“ value=“interval.cz“ />
  <add key=“MapWebRequestProxy“ value=““ />
  <add key=“MapWebRequestProxyUserName“ value=““ />
  <add key=“MapWebRequestProxyPassword“ value=““ />
</appSettings>

MapScriptProxyPath –  URL poskytovatele služby
MapClientHost – hostname, na které bude obsah poskytovatele zveřejněn (poskytovatel je může využít při konstrukci skriptů pro klienta)
MapWebRequestProxy – URL proxy v naší síti, pokud ji používáme pro přístup k poskytovateli služby
MapWebRequestProxyUserName – uživatelské jméno pro autentifikaci v interní proxy, pokud ji používáme
MapWebRequestProxyPassword – heslo  pro autentifikaci v interní proxy, pokud ji používáme

Přeposílátko jako HTTP Handler

Handler bude mít klasickou deklaraci:

using System;
using System.Web;
using System.Net;
using System.IO;
using System.Text;
using System.Configuration;
namespace Interval.CZ.Web.RemoteServices.Provider.CZ
{
  public sealed class ScriptProxy : IHttpHandler
  {
    public bool IsReusable
    {
      get
      {
        return true;
      }
    }

  public void ProcessRequest(HttpContext context)
  {
  }
}

Vytvoříme si statické proměnné, do kterých budou načteny konfigurační hodnoty:

public sealed class ScriptProxy : IHttpHandler
{
  private static String MapScriptProxyPath = ConfigurationManager.AppSettings[„MapScriptProxyPath“];
  private static String MapClientHost = ConfigurationManager.AppSettings[„MapClientHost“];
  private static String MapWebRequestProxy = ConfigurationManager.AppSettings[„MapWebRequestProxy“];
  private static String MapWebRequestProxyUserName = ConfigurationManager.AppSettings[„MapWebRequestProxyUserName“];
  private static String MapWebRequestProxyPassword = ConfigurationManager.AppSettings[„MapWebRequestProxyPassword“];

  public bool IsReusable
  {
    get
    {
      return true;
    }
  }
  public void ProcessRequest(HttpContext context)
  {
  }
}

A nakonec doplníme vlastní obslužnou část, která stáhne obsah od poskytovatele a přepošle ho v odpovědi na požadavek:

public void ProcessRequest(HttpContext context)
{
  Int32 queryPos = context.Request.RawUrl.IndexOf(‚?‘);
  String targetUrl = (queryPos != -1) ? String.Concat(MapScriptProxyPath, context.Request.Path.Substring(context.Request.ApplicationPath.Length + 1), context.Request.RawUrl.Substring(queryPos)) : String.Concat(MapScriptProxyPath, context.Request.Path.Substring(context.Request.ApplicationPath.Length + 1));
  HttpWebRequest myRequest = (HttpWebRequest) WebRequest.Create(new Uri(targetUrl));
  if (!String.IsNullOrEmpty(MapClientHost))
    myRequest.Headers.Add(„HTTP_X_SERVER“,MapClientHost);
  if (!String.IsNullOrEmpty(MapWebRequestProxy))
  {
    myRequest.Proxy = new WebProxy(MapWebRequestProxy);
    myRequest.Proxy.Credentials = new NetworkCredential(MapWebRequestProxyUserName,MapWebRequestProxyPassword);
  }
  try
  {
    using (System.Net.HttpWebResponse myResponse = (HttpWebResponse) myRequest.GetResponse())
    {
      if (myRequest.HaveResponse && myResponse.ContentLength != 0)
      {
        context.Response.Buffer = true;
        context.Response.ContentType = myResponse.ContentType;
        context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
        context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
        DateTime lastModified = myResponse.LastModified;
        if (lastModified > DateTime.Now)
          lastModified = DateTime.Now;
        context.Response.Cache.SetLastModified(lastModified);
        using (Stream responseStream = myResponse.GetResponseStream())
        {
          byte[] buffer = new byte[0x1000];
          int bytes;
          while((bytes = responseStream.Read(buffer, 0, buffer.Length)) > 0)
          {
            context.Response.OutputStream.Write(buffer, 0, bytes);
          }
        }
      }
      else
      {
        context.Response.Clear();
        context.Response.StatusCode = 502;
        context.Response.StatusDescription = „Bad Gateway“;
      }
    }
  }
  catch (System.Net.WebException eWX)
  {
    context.Response.Clear();
    context.Response.StatusCode = 504;
    context.Response.StatusDescription = „Gateway Timeout“;
  }
  catch (Exception eX)
  {
    context.Response.Clear();
    context.Response.StatusCode = 503;
    context.Response.StatusDescription = „Unavailable“;
  }
}

Nejprve na začátku připravíme URL pro HttpWebRequest na server poskytovatele – řešíme zde, zda požadavek od klienta obsahuje QueryString. Pokud ano, tak jej ještě musíme správně připojit k cílovému URL.

Pokud naše síť nemá přímý přístup do internetu nebo k poskytovateli API, pak se ještě provede autentizace k síťové proxy.
Odpověď serveru pak přijmeme metodou GetResponse(). Následně převezmeme obsah některých hlaviček a předáme je také na výstup „přeposílátka“.
V některých případech se hodí sdělit původnímu serveru jaká je doména (hostname) klientské stránky. Ve vlastní hlavičce HTTP_X_SERVER můžeme hostname posílat zdrojovému serveru, aby mohl případně upravit cesty v odesílaných skriptech nebo provádět nějaké customizace obsahu.
Co se týká hlaviček pro řízení cache, zvláštní pozornosti je potřeba u hlavičky Last-Modified. Může se stát, že vzdálený server bude mít čas dřívější než náš server (může jít o milisekundy i o sekundy či minuty) – a v tom případě bychom odesílali údaj o tom, že dokument byl naposledy modifikován v budoucnosti. To z principu není možné a vyvolalo by to výjimku, proto čas ze vzdáleného serveru testujeme a pokud by nastal tento případ, pak odešleme aktuální čas našeho serveru.
Nakonec „slízneme“ stream odpovědi pomocí GetResponseStream() a postupně jej po 1000 bytech upouštíme do výstupního bufferu odpovědi.

Aplikace je vlastně velmi jednoduchá, takže si můžeme dovolit „požírat“ výjimky – spoléháme na řízení chyb u klienta přes HTTP Status kódy, jinak by se samozřejmě patřilo chybová hlášení řádně ošetřit.

Možnosti kešování

Pokud obsah mash-upu nemusí být nutně online – nejedná se o interaktivní aplikaci, můžeme naopak vynutit kešování u klienta přidáním hlaviček:

context.Response.Cache.SetCacheability(HttpCacheability.Public);
if (!String.IsNullOrEmpty(myResponse.Headers[„Expires“]))
  context.Response.Cache.SetExpires(lastModified.AddMinutes(60));
if (!String.IsNullOrEmpty(myResponse.Headers[„Cache-Control“]))
  context.Response.Cache.SetMaxAge(new TimeSpan(0,0,3600));

Konfigurace a zprovoznění přeposílátka

Konfigurace „přeposílaných“ URL definujeme v souboru Web.config – jde vlastně o nastavení cesty, které se má handler „chopit“:

<httpHandlers>
  <add verb=“GET“ path=“MapAPI/map.php“ type=“Interval.CZ.Web.RemoteServices.Provider.CZ.ScriptProxy“ />
  <add verb=“GET“ path=“MapAPI/detail.php“ type=“Interval.CZ.Web.RemoteServices.Provider.CZ.ScriptProxy“ />
</httpHandlers>

Jak vidíme, soubory poskytovatele mají příponu .php. To ovšem nijak nebrání tomu, abychom i na tyto namapovali náš handler a jejich výstup přeposílali do prohlížeče z našeho serveru. Samozřejmě, zvláštní přípony jako třeba .php je potřeba na serveru IIS verze 6 a nižší také nakonfigurovat tak, aby byly obsluhovány .net modulem. Určitě není špatným tipem vyhradit pro přeposílátko zvláštní aplikační adresář – pro snadnější konfiguraci a oddělení od ostatních spolu nesouvísejících částí aplikace.

Souhrn

Zde popsané přeposílátko nám přináší omezení bezpečnostních rizik a umožnění bezpečného provozu i přes HTTPS. Přeposílátku můžeme vdechnout i nějakou logiku, kterou pak můžeme přímo za běhu modifikovat předávaný obsah, například některé části vypustit nebo nahradit. Při výpadku služby také můžeme zobrazit uživatelsky přívětivé hlášení narozdíl od klasických mash-upů, které se při výpadku chovají nepředvídatelně.

Stažení ukázkové website se ScriptProxy

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *