X-Git-Url: http://www.project-moonshot.org/gitweb/?a=blobdiff_plain;f=isapi_shib%2Fisapi_shib.cpp;h=f30cab66aa29b5519d74a55a884d6e17b4da5ae1;hb=3ea5b63fd99233bc4aaff07397c5fec840e5a750;hp=bb51f4d94bdffb47ee2594814d02380fef1aa39a;hpb=ec5b186e96c7196cfab29bcd3bf831e0b7e85278;p=shibboleth%2Fcpp-sp.git diff --git a/isapi_shib/isapi_shib.cpp b/isapi_shib/isapi_shib.cpp index bb51f4d..f30cab6 100644 --- a/isapi_shib/isapi_shib.cpp +++ b/isapi_shib/isapi_shib.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2001-2005 Internet2 - * + * Copyright 2001-2009 Internet2 + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -57,6 +57,10 @@ namespace { { chLatin_n, chLatin_o, chLatin_r, chLatin_m, chLatin_a, chLatin_l, chLatin_i, chLatin_z, chLatin_e, chLatin_R, chLatin_e, chLatin_q, chLatin_u, chLatin_e, chLatin_s, chLatin_t, chNull }; + static const XMLCh safeHeaderNames[] = + { chLatin_s, chLatin_a, chLatin_f, chLatin_e, chLatin_H, chLatin_e, chLatin_a, chLatin_d, chLatin_e, chLatin_r, + chLatin_N, chLatin_a, chLatin_m, chLatin_e, chLatin_s, chNull + }; static const XMLCh Site[] = { chLatin_S, chLatin_i, chLatin_t, chLatin_e, chNull }; struct site_t { @@ -81,13 +85,16 @@ namespace { string m_scheme,m_port,m_sslport,m_name; set m_aliases; }; - + HINSTANCE g_hinstDLL; ShibTargetConfig* g_Config = NULL; map g_Sites; bool g_bNormalizeRequest = true; - string g_unsetHeaderValue; + string g_unsetHeaderValue,g_spoofKey; + set g_allowedSchemes; bool g_checkSpoofing = true; + bool g_catchAll = true; + bool g_bSafeHeaderNames = false; } BOOL LogEvent( @@ -98,7 +105,7 @@ BOOL LogEvent( LPCSTR message) { LPCSTR messages[] = {message, NULL}; - + HANDLE hElog = RegisterEventSource(lpUNCServerName, "Shibboleth ISAPI Filter"); BOOL res = ReportEvent(hElog, wType, 0, dwEventID, lpUserSid, 1, 0, messages, NULL); return (DeregisterEventSource(hElog) && res); @@ -115,7 +122,7 @@ extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer) { if (!pVer) return FALSE; - + if (!g_Config) { LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, @@ -143,10 +150,7 @@ extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer) return TRUE; } -#ifndef _DEBUG - try - { -#endif + try { LPCSTR schemadir=getenv("SHIBSCHEMAS"); if (!schemadir) schemadir=SHIB_SCHEMAS; @@ -174,24 +178,56 @@ extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer) "Filter startup failed to load configuration, check native log for help."); return FALSE; } - + // Access the implementation-specifics for site mappings. IConfig* conf=g_Config->getINI(); Locker locker(conf); const IPropertySet* props=conf->getPropertySet("Local"); if (props) { - pair unsetValue=props->getString("unsetHeaderValue"); - if (unsetValue.first) - g_unsetHeaderValue = unsetValue.second; - pair checkSpoofing=props->getBool("checkSpoofing"); - if (checkSpoofing.first && !checkSpoofing.second) - g_checkSpoofing = false; + pair flag=props->getBool("checkSpoofing"); + g_checkSpoofing = !flag.first || flag.second; + flag=props->getBool("catchAll"); + g_catchAll = !flag.first || flag.second; + + pair str=props->getString("unsetHeaderValue"); + if (str.first) + g_unsetHeaderValue = str.second; + + str=props->getString("allowedSchemes"); + if (str.first) { + string schemes=str.second; + unsigned int j=0; + for (unsigned int i=0; i < schemes.length(); i++) { + if (schemes.at(i)==' ') { + g_allowedSchemes.insert(schemes.substr(j, i-j)); + j = i+1; + } + } + g_allowedSchemes.insert(schemes.substr(j, schemes.length()-j)); + } + + if (g_checkSpoofing) { + str = props->getString("spoofKey"); + if (str.first) + g_spoofKey = str.second; + else { + LogEvent(NULL, EVENTLOG_WARNING_TYPE, 2100, NULL, + "Filter generating a pseudorandom anti-spoofing key, consider setting spoofKey yourself."); + srand(time(NULL)); + ostringstream keystr; + keystr << rand() << rand() << rand() << rand(); + g_spoofKey = keystr.str(); + } + } + const DOMElement* impl=saml::XML::getFirstChildElement( props->getElement(),shibtarget::XML::SHIBTARGET_NS,Implementation ); if (impl && (impl=saml::XML::getFirstChildElement(impl,shibtarget::XML::SHIBTARGET_NS,ISAPI))) { - const XMLCh* flag=impl->getAttributeNS(NULL,normalizeRequest); - g_bNormalizeRequest=(!flag || !*flag || *flag==chDigit_1 || *flag==chLatin_t); + const XMLCh* ch=impl->getAttributeNS(NULL,normalizeRequest); + g_bNormalizeRequest=(!ch || !*ch || *ch==chDigit_1 || *ch==chLatin_t); + ch=impl->getAttributeNS(NULL,safeHeaderNames); + g_bSafeHeaderNames=(ch && (*ch==chDigit_1 || *ch==chLatin_t)); impl=saml::XML::getFirstChildElement(impl,shibtarget::XML::SHIBTARGET_NS,Site); while (impl) { auto_ptr_char id(impl->getAttributeNS(NULL,id)); @@ -201,14 +237,15 @@ extern "C" BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer) } } } -#ifndef _DEBUG + if (g_allowedSchemes.empty()) { + g_allowedSchemes.insert("https"); + g_allowedSchemes.insert("http"); + } } - catch (...) - { + catch (exception&) { LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, "Filter startup failed with an exception."); return FALSE; } -#endif pVer->dwFilterVersion=HTTP_FILTER_REVISION; strncpy(pVer->lpszFilterDesc,"Shibboleth ISAPI Filter",SF_MAX_FILTER_DESC_LEN); @@ -350,10 +387,19 @@ class ShibTargetIsapiF : public ShibTarget PHTTP_FILTER_PREPROC_HEADERS m_pn; string m_cookie; dynabuf m_allhttp; + bool m_firsttime; + + void checkString(const string& s, const char* msg) { + string::const_iterator e = s.end(); + for (string::const_iterator i=s.begin(); i!=e; ++i) { + if (iscntrl(*i)) + throw FatalProfileException(msg); + } + } public: ShibTargetIsapiF(PHTTP_FILTER_CONTEXT pfc, PHTTP_FILTER_PREPROC_HEADERS pn, const site_t& site) - : m_pfc(pfc), m_pn(pn), m_allhttp(4096) { + : m_pfc(pfc), m_pn(pn), m_allhttp(4096), m_firsttime(true) { // URL path always come from IIS. dynabuf url(256); @@ -371,7 +417,7 @@ public: strncpy(port,site.m_port.c_str(),10); static_cast(port)[10]=0; } - + // Scheme may come from site def or be derived from IIS. const char* scheme=site.m_scheme.c_str(); if (!scheme || !*scheme || !g_bNormalizeRequest) @@ -389,7 +435,16 @@ public: if (site.m_name!=host && site.m_aliases.find(host)==site.m_aliases.end()) host=site.m_name.c_str(); - init(scheme, host, atoi(port), url, content_type, remote_addr, method); + init(scheme, host, atoi(port), url, content_type, remote_addr, method); + + if (!g_spoofKey.empty()) { + GetHeader(pn, pfc, "ShibSpoofCheck:", url, 32, false); + if (!url.empty() && g_spoofKey == (char*)url) + m_firsttime = false; + } + + if (!m_firsttime) + log(LogLevelDebug, "ISAPI filter running more than once"); } ~ShibTargetIsapiF() {} @@ -401,40 +456,68 @@ public: GetHeader(m_pn, m_pfc, "Cookie:", buf, 128, false); return buf.empty() ? "" : buf; } - + string makeSafeHeader(const char* rawname) const { + string hdr; + for (; *rawname; ++rawname) { + if (isalnum(*rawname)) + hdr += *rawname; + } + return hdr + ':'; + } virtual void clearHeader(const string &name) { - if (g_checkSpoofing) { + if (g_checkSpoofing && m_firsttime) { if (m_allhttp.empty()) GetServerVariable(m_pfc,"ALL_HTTP",m_allhttp,4096); // Map to the expected CGI variable name. string transformed("HTTP_"); - const char* pch = name.c_str(); - while (*pch) { - transformed += (isalnum(*pch) ? toupper(*pch) : '_'); - pch++; + if (g_bSafeHeaderNames) { + string temp = makeSafeHeader(name.c_str()); + for (const char* pch = temp.c_str(); *pch; ++pch) { + if (isalnum(*pch)) + transformed += toupper(*pch); + } + } + else { + for (const char* pch = name.c_str(); *pch; ++pch) + transformed += (isalnum(*pch) ? toupper(*pch) : '_'); + transformed += ':'; } - transformed += ':'; if (strstr(m_allhttp, transformed.c_str())) - throw SAMLException("Attempt to spoof header ($1) was detected.", params(1, name.c_str())); + throw SAMLException("Attempt to spoof header ($1) was detected.", params(1, transformed.c_str())); + } + if (g_bSafeHeaderNames) { + string hdr = makeSafeHeader(name.c_str()); + m_pn->SetHeader(m_pfc, const_cast(hdr.c_str()), const_cast(g_unsetHeaderValue.c_str())); + } + else if (name == "REMOTE_USER") { + m_pn->SetHeader(m_pfc, "remote-user:", const_cast(g_unsetHeaderValue.c_str())); + m_pn->SetHeader(m_pfc, "remote_user:", const_cast(g_unsetHeaderValue.c_str())); + } + else { + string hdr = name + ':'; + m_pn->SetHeader(m_pfc, const_cast(hdr.c_str()), const_cast(g_unsetHeaderValue.c_str())); } - string hdr = (name=="REMOTE_USER" ? "remote-user" : name) + ":"; - m_pn->SetHeader(m_pfc, const_cast(hdr.c_str()), const_cast(g_unsetHeaderValue.c_str())); } virtual void setHeader(const string &name, const string &value) { - string hdr = name + ":"; - m_pn->SetHeader(m_pfc, const_cast(hdr.c_str()), - const_cast(value.c_str())); + string hdr = g_bSafeHeaderNames ? makeSafeHeader(name.c_str()) : (name + ':'); + m_pn->SetHeader(m_pfc, const_cast(hdr.c_str()), const_cast(value.c_str())); } virtual string getHeader(const string &name) { - string hdr = name + ":"; - dynabuf buf(1024); + string hdr = g_bSafeHeaderNames ? makeSafeHeader(name.c_str()) : (name + ':'); + dynabuf buf(256); GetHeader(m_pn, m_pfc, const_cast(hdr.c_str()), buf, 1024, false); - return string(buf); + return string(buf.empty() ? "" : buf); } virtual void setRemoteUser(const string &user) { setHeader(string("remote-user"), user); + if (m_pfc->pFilterContext) { + if (user.empty()) + m_pfc->pFilterContext = NULL; + else if (m_pfc->pFilterContext = m_pfc->AllocMem(m_pfc, sizeof(char) * (user.length() + 1), NULL)) + strcpy(reinterpret_cast(m_pfc->pFilterContext), user.c_str()); + } } virtual string getRemoteUser(void) { return getHeader(string("remote-user")); @@ -444,9 +527,12 @@ public: int code=200, const string& content_type="text/html", const Iterator& headers=EMPTY(header_t)) { + checkString(content_type, "Detected control character in a response header."); string hdr = string ("Connection: close\r\nContent-type: ") + content_type + "\r\n"; while (headers.hasNext()) { const header_t& h=headers.next(); + checkString(h.first, "Detected control character in a response header."); + checkString(h.second, "Detected control character in a response header."); hdr += h.first + ": " + h.second + "\r\n"; } hdr += "\r\n"; @@ -462,6 +548,9 @@ public: return (void*)SF_STATUS_REQ_FINISHED; } virtual void* sendRedirect(const string& url) { + checkString(url, "Detected control character in an attempted redirect."); + if (g_allowedSchemes.find(url.substr(0, url.find(':'))) == g_allowedSchemes.end()) + throw FatalProfileException("Invalid scheme in attempted redirect."); // XXX: Don't support the httpRedirect option, yet. string hdrs=m_cookie + string("Location: ") + url + "\r\n" "Content-Type: text/html\r\n" @@ -508,16 +597,14 @@ DWORD WriteClientError(PHTTP_FILTER_CONTEXT pfc, const char* msg) extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pvNotification) { // Is this a log notification? - if (notificationType==SF_NOTIFY_LOG) - { + if (notificationType==SF_NOTIFY_LOG) { if (pfc->pFilterContext) - ((PHTTP_FILTER_LOG)pvNotification)->pszClientUserName=static_cast(pfc->pFilterContext); + ((PHTTP_FILTER_LOG)pvNotification)->pszClientUserName=reinterpret_cast(pfc->pFilterContext); return SF_STATUS_REQ_NEXT_NOTIFICATION; } PHTTP_FILTER_PREPROC_HEADERS pn=(PHTTP_FILTER_PREPROC_HEADERS)pvNotification; - try - { + try { // Determine web site number. This can't really fail, I don't think. dynabuf buf(128); GetServerVariable(pfc,"INSTANCE_ID",buf,10); @@ -526,7 +613,7 @@ extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificat map::const_iterator map_i=g_Sites.find(static_cast(buf)); if (map_i==g_Sites.end()) return SF_STATUS_REQ_NEXT_NOTIFICATION; - + ostringstream threadid; threadid << "[" << getpid() << "] isapi_shib" << '\0'; saml::NDC ndc(threadid.str().c_str()); @@ -535,6 +622,8 @@ extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificat // "false" because we don't override the Shib settings pair res = stf.doCheckAuthN(); + if (!g_spoofKey.empty()) + pn->SetHeader(pfc, "ShibSpoofCheck:", const_cast(g_spoofKey.c_str())); if (res.first) return (DWORD)res.second; // "false" because we don't override the Shib settings @@ -555,19 +644,19 @@ extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificat else return WriteClientError(pfc,"Shibboleth Filter detected unexpected IIS error."); } - catch (SAMLException& e) { + catch (exception& e) { LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, e.what()); return WriteClientError(pfc,"Shibboleth Filter caught an exception, ask administrator to check Event Log for details."); } -#ifndef _DEBUG catch(...) { - return WriteClientError(pfc,"Shibboleth Filter caught an unknown exception."); + if (g_catchAll) + return WriteClientError(pfc,"Shibboleth Filter caught an unknown exception."); + throw; } -#endif return WriteClientError(pfc,"Shibboleth Filter reached unreachable code, save my walrus!"); } - + /****************************************************************************/ // ISAPI Extension @@ -593,7 +682,15 @@ class ShibTargetIsapiE : public ShibTarget { LPEXTENSION_CONTROL_BLOCK m_lpECB; string m_cookie; - + + void checkString(const string& s, const char* msg) { + string::const_iterator e = s.end(); + for (string::const_iterator i=s.begin(); i!=e; ++i) { + if (iscntrl(*i)) + throw FatalProfileException(msg); + } + } + public: ShibTargetIsapiE(LPEXTENSION_CONTROL_BLOCK lpECB, const site_t& site) { dynabuf ssl(5); @@ -638,20 +735,20 @@ public: * the server is set up for proper PATH_INFO handling, or "IIS sucks rabid weasels mode", * which is the default. No perfect way to tell, but we can take a good guess by checking * whether the URL is a substring of the PATH_INFO: - * + * * e.g. for /Shibboleth.sso/SAML/POST - * + * * Bad mode (default): * URL: /Shibboleth.sso * PathInfo: /Shibboleth.sso/SAML/POST - * + * * Good mode: * URL: /Shibboleth.sso * PathInfo: /SAML/POST */ - + string fullurl; - + // Clearly we're only in bad mode if path info exists at all. if (lpECB->lpszPathInfo && *(lpECB->lpszPathInfo)) { if (strstr(lpECB->lpszPathInfo,url)) @@ -665,7 +762,7 @@ public: else { fullurl = url; } - + // For consistency with Apache, let's add the query string. if (lpECB->lpszQueryString && *(lpECB->lpszQueryString)) { fullurl+='?'; @@ -679,8 +776,6 @@ public: virtual void log(ShibLogLevel level, const string &msg) { ShibTarget::log(level,msg); - if (level == LogLevelError) - LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, msg.c_str()); } virtual string getCookies() const { dynabuf buf(128); @@ -697,10 +792,10 @@ public: virtual string getPostData(void) { if (m_lpECB->cbTotalBytes > 1024*1024) // 1MB? throw FatalProfileException("Blocked too-large a submission to profile endpoint."); - else if (m_lpECB->cbTotalBytes != m_lpECB->cbAvailable) { - string cgistr; + else if (m_lpECB->cbTotalBytes > m_lpECB->cbAvailable) { + string cgistr(reinterpret_cast(m_lpECB->lpbData),m_lpECB->cbAvailable); char buf[8192]; - DWORD datalen=m_lpECB->cbTotalBytes; + DWORD datalen=m_lpECB->cbTotalBytes - m_lpECB->cbAvailable; while (datalen) { DWORD buflen=8192; BOOL ret = m_lpECB->ReadClient(m_lpECB->ConnID, buf, &buflen); @@ -711,16 +806,20 @@ public: } return cgistr; } - else + else { return string(reinterpret_cast(m_lpECB->lpbData),m_lpECB->cbAvailable); + } } virtual void* sendPage( const string &msg, int code=200, const string& content_type="text/html", const Iterator& headers=EMPTY(header_t)) { - string hdr = string ("Connection: close\r\nContent-type: ") + content_type + "\r\n"; + checkString(content_type, "Detected control character in a response header."); + string hdr = m_cookie + "Connection: close\r\nContent-type: " + content_type + "\r\n"; for (int k = 0; k < headers.size(); k++) { + checkString(headers[k].first, "Detected control character in a response header."); + checkString(headers[k].second, "Detected control character in a response header."); hdr += headers[k].first + ": " + headers[k].second + "\r\n"; } hdr += "\r\n"; @@ -737,6 +836,9 @@ public: } virtual void* sendRedirect(const string& url) { // XXX: Don't support the httpRedirect option, yet. + checkString(url, "Detected control character in an attempted redirect."); + if (g_allowedSchemes.find(url.substr(0, url.find(':'))) == g_allowedSchemes.end()) + throw FatalProfileException("Invalid scheme in attempted redirect."); string hdrs = m_cookie + "Location: " + url + "\r\n" "Content-Type: text/html\r\n" "Content-Length: 40\r\n" @@ -785,7 +887,7 @@ extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB) ShibTargetIsapiE ste(lpECB, map_i->second); pair res = ste.doHandler(); if (res.first) return (DWORD)res.second; - + return WriteClientError(lpECB, "Shibboleth Extension failed to process request"); } @@ -798,15 +900,15 @@ extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB) else return WriteClientError(lpECB,"Server detected unexpected IIS error."); } - catch (SAMLException& e) { + catch (exception& e) { LogEvent(NULL, EVENTLOG_ERROR_TYPE, 2100, NULL, e.what()); return WriteClientError(lpECB,"Shibboleth Extension caught an exception, check Event Log for details."); } -#ifndef _DEBUG catch(...) { - return WriteClientError(lpECB,"Shibboleth Extension caught an unknown exception."); + if (g_catchAll) + return WriteClientError(lpECB,"Shibboleth Extension caught an unknown exception."); + throw; } -#endif // If we get here we've got an error. return HSE_STATUS_ERROR;