// Since we're passing the ACS by value, we need to compute the return URL,
// so we'll need the target resource for real.
recoverRelayState(app, request, request, target, false);
+ app.limitRedirect(request, target.c_str());
acClass = getString("authnContextClassRef", request);
}
);
}
+ const char* returnloc = httpRequest.getParameter("return");
+ if (returnloc)
+ application.limitRedirect(httpRequest, returnloc);
+
// Log the request.
scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(application, &httpRequest, session));
if (logout_event) {
application.getServiceProvider().getTransactionLog()->write(*logout_event);
}
- const URLEncoder* urlenc = XMLToolingConfig::getConfig().getURLEncoder();
- const char* returnloc = httpRequest.getParameter("return");
auto_ptr_char dest(ep->getLocation());
string req=string(dest.get()) + (strchr(dest.get(),'?') ? '&' : '?') + "wa=wsignout1.0";
- if (returnloc)
- req += "&wreply=" + urlenc->encode(returnloc);
+ if (returnloc) {
+ req += "&wreply=";
+ if (*returnloc == '/') {
+ string s(returnloc);
+ httpRequest.absolutize(s);
+ req += XMLToolingConfig::getConfig().getURLEncoder()->encode(s.c_str());
+ }
+ else {
+ req += XMLToolingConfig::getConfig().getURLEncoder()->encode(returnloc);
+ }
+ }
ret.second = httpResponse.sendRedirect(req.c_str());
ret.first = true;
}
}
- if (param)
- return make_pair(true, request.sendRedirect(param));
+ if (param) {
+ if (*param == '/') {
+ string p(param);
+ request.absolutize(p);
+ return make_pair(true, request.sendRedirect(p.c_str()));
+ }
+ else {
+ app.limitRedirect(request, param);
+ return make_pair(true, request.sendRedirect(param));
+ }
+ }
return sendLogoutPage(app, request, request, "global");
}
</restriction>
</simpleType>
- <simpleType name="relayStateLimitType">
+ <simpleType name="redirectLimitType">
<restriction base="string">
<enumeration value="none"/>
<enumeration value="exact"/>
<enumeration value="host"/>
<enumeration value="whitelist"/>
+ <enumeration value="exact+whitelist"/>
+ <enumeration value="host+whitelist"/>
</restriction>
</simpleType>
<attribute name="postTemplate" type="conf:string"/>
<attribute name="postExpire" type="boolean"/>
<attribute name="relayState" type="conf:string"/>
- <attribute name="relayStateLimit" type="conf:relayStateLimitType"/>
+ <attribute name="relayStateLimit" type="conf:redirectLimitType"/>
<attribute name="relayStateWhitelist" type="conf:listOfURIs"/>
+ <attribute name="redirectLimit" type="conf:redirectLimitType"/>
+ <attribute name="redirectWhitelist" type="conf:listOfURIs"/>
<anyAttribute namespace="##other" processContents="lax"/>
</complexType>
const vector<const Handler*>& handlers = getAssertionConsumerServicesByBinding(b.get());
return handlers.empty() ? nullptr : handlers.front();
}
+
+void Application::limitRedirect(const GenericRequest& request, const char* url) const
+{
+}
namespace xmltooling {
class XMLTOOL_API CredentialResolver;
+ class XMLTOOL_API GenericRequest;
class XMLTOOL_API RWLock;
class XMLTOOL_API SOAPTransport;
class XMLTOOL_API StorageService;
* @param handlers array to populate
*/
virtual void getHandlers(std::vector<const Handler*>& handlers) const=0;
+
+ /**
+ * Checks a proposed redirect URL against application-specific settings for legal redirects,
+ * such as same-host restrictions or whitelisted domains, and raises a SecurityPolicyException
+ * in the event of a violation.
+ *
+ * @param request the request leading to the redirect
+ * @param url an absolute URL to validate
+ */
+ virtual void limitRedirect(const xmltooling::GenericRequest& request, const char* url) const;
};
#if defined (_MSC_VER)
using namespace std;
namespace shibsp {
-
SHIBSP_DLLLOCAL PluginManager< Handler,string,pair<const DOMElement*,const char*> >::Factory SAML1ConsumerFactory;
SHIBSP_DLLLOCAL PluginManager< Handler,string,pair<const DOMElement*,const char*> >::Factory SAML2ConsumerFactory;
SHIBSP_DLLLOCAL PluginManager< Handler,string,pair<const DOMElement*,const char*> >::Factory SAML2ArtifactResolutionFactory;
buf += (DIGITS[0x0F & b2]);
}
}
-
- void SHIBSP_DLLLOCAL limitRelayState(
- Category& log, const Application& application, const HTTPRequest& httpRequest, const char* relayState
- ) {
- const PropertySet* sessionProps = application.getPropertySet("Sessions");
- if (sessionProps) {
- pair<bool,const char*> relayStateLimit = sessionProps->getString("relayStateLimit");
- if (relayStateLimit.first && strcmp(relayStateLimit.second, "none")) {
- vector<string> whitelist;
- if (!strcmp(relayStateLimit.second, "exact")) {
- // Scheme and hostname have to match.
- if (httpRequest.isDefaultPort()) {
- whitelist.push_back(string(httpRequest.getScheme()) + httpRequest.getHostname() + '/');
- }
- whitelist.push_back(
- string(httpRequest.getScheme()) + "://" + httpRequest.getHostname() + ':' + lexical_cast<string>(httpRequest.getPort()) + '/'
- );
- }
- else if (!strcmp(relayStateLimit.second, "host")) {
- // Allow any scheme or port.
- whitelist.push_back(string("https://") + httpRequest.getHostname() + '/');
- whitelist.push_back(string("http://") + httpRequest.getHostname() + '/');
- whitelist.push_back(string("https://") + httpRequest.getHostname() + ':');
- whitelist.push_back(string("http://") + httpRequest.getHostname() + ':');
- }
- else if (!strcmp(relayStateLimit.second, "whitelist")) {
- // Literal set of comparisons to use.
- pair<bool,const char*> whitelistval = sessionProps->getString("relayStateWhitelist");
- if (whitelistval.first) {
- string dup(whitelistval.second);
- split(whitelist, dup, is_space(), algorithm::token_compress_on);
- }
- }
- else {
- log.warn("unrecognized relayStateLimit policy (%s), blocked redirect to (%s)", relayStateLimit.second, relayState);
- throw opensaml::SecurityPolicyException("Unrecognized relayStateLimit setting.");
- }
-
- static bool (*startsWithI)(const char*,const char*) = XMLString::startsWithI;
- if (find_if(whitelist.begin(), whitelist.end(),
- boost::bind(startsWithI, relayState, boost::bind(&string::c_str, _1))) == whitelist.end()) {
- log.warn("relayStateLimit policy (%s), blocked redirect to (%s)", relayStateLimit.second, relayState);
- throw opensaml::SecurityPolicyException("Blocked unacceptable redirect location.");
- }
- }
- }
- }
};
void SHIBSP_API shibsp::registerHandlers()
DDF postData = recoverPostData(application, httpRequest, httpResponse, relayState.c_str());
DDFJanitor postjan(postData);
recoverRelayState(application, httpRequest, httpResponse, relayState);
- limitRelayState(m_log, application, httpRequest, relayState.c_str());
+ application.limitRedirect(httpRequest, relayState.c_str());
implementProtocol(application, httpRequest, httpResponse, *policy, nullptr, *msg);
auto_ptr_char issuer(policy->getIssuer() ? policy->getIssuer()->getName() : nullptr);
// Route back to return location specified, or use the local template.
const char* dest = httpRequest.getParameter("return");
if (dest) {
- limitRelayState(m_log, application, httpRequest, dest);
+ // Relative URLs get promoted, absolutes get validated.
+ if (*dest == '/') {
+ string d(dest);
+ httpRequest.absolutize(d);
+ return make_pair(true, httpResponse.sendRedirect(d.c_str()));
+ }
+ application.limitRedirect(httpRequest, dest);
return make_pair(true, httpResponse.sendRedirect(dest));
}
return sendLogoutPage(application, httpRequest, httpResponse, "local");
if (logoutResponse) {
if (!policy->isAuthenticated()) {
SecurityPolicyException ex("Security of LogoutResponse not established.");
- if (policy->getIssuerMetadata())
- annotateException(&ex, policy->getIssuerMetadata()); // throws it
- ex.raise();
+ annotateException(&ex, policy->getIssuerMetadata()); // throws it
}
if (logout_event) {
}
if (!relayState.empty()) {
- limitRelayState(m_log, application, request, relayState.c_str());
+ application.limitRedirect(request, relayState.c_str());
return make_pair(true, response.sendRedirect(relayState.c_str()));
}
else {
const char* returnloc = httpRequest.getParameter("return");
if (returnloc) {
- limitRelayState(m_log, application, httpRequest, returnloc);
- ret.second = httpResponse.sendRedirect(returnloc);
+ // Relative URLs get promoted, absolutes get validated.
+ if (*returnloc == '/') {
+ string loc(returnloc);
+ httpRequest.absolutize(loc);
+ ret.second = httpResponse.sendRedirect(loc.c_str());
+ }
+ else {
+ application.limitRedirect(httpRequest, returnloc);
+ ret.second = httpResponse.sendRedirect(returnloc);
+ }
ret.first = true;
}
- ret = sendLogoutPage(application, httpRequest, httpResponse, "global");
+ else {
+ ret = sendLogoutPage(application, httpRequest, httpResponse, "global");
+ }
}
}
string relayState;
const char* returnloc = httpRequest.getParameter("return");
if (returnloc) {
- limitRelayState(m_log, application, httpRequest, returnloc);
+ application.limitRedirect(httpRequest, returnloc);
relayState = returnloc;
+ httpRequest.absolutize(relayState);
preserveRelayState(application, httpResponse, relayState);
}
*/
FatalProfileException ex("Incoming message was not a samlp:ManageNameIDRequest.");
- if (policy->getIssuerMetadata())
- annotateException(&ex, policy->getIssuerMetadata()); // throws it
- ex.raise();
+ annotateException(&ex, policy->getIssuerMetadata()); // throws it
return make_pair(false, 0L); // never happen, satisfies compiler
#else
throw ConfigurationException("Cannot process NameID mgmt message using lite version of shibsp library.");
// Always need to recover target URL to compute handler below.
recoverRelayState(app, request, request, target, false);
- limitRelayState(m_log, app, request, target.c_str());
+ app.limitRedirect(request, target.c_str());
pair<bool,bool> flag = getBool("isPassive", request);
isPassive = (flag.first && flag.second);
// Since we're passing the ACS by value, we need to compute the return URL,
// so we'll need the target resource for real.
recoverRelayState(app, request, request, target, false);
- limitRelayState(m_log, app, request, target.c_str());
+ app.limitRedirect(request, target.c_str());
}
else {
// Check for a hardwired target value in the map or handler.
// Since we're passing the ACS by value, we need to compute the return URL,
// so we'll need the target resource for real.
recoverRelayState(request.getApplication(), request, request, target, false);
- limitRelayState(m_log, request.getApplication(), request, target.c_str());
+ request.getApplication().limitRedirect(request, target.c_str());
prop.second = request.getParameter("discoveryURL");
if (prop.second && *prop.second)
#endif
#include <algorithm>
#include <boost/bind.hpp>
+#include <boost/lexical_cast.hpp>
#include <boost/scoped_ptr.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/algorithm/string.hpp>
# include "attribute/resolver/AttributeResolver.h"
# include "security/PKIXTrustEngine.h"
# include "security/SecurityPolicyProvider.h"
-# include <saml/SAMLConfig.h>
+# include <saml/exceptions.h>
# include <saml/version.h>
+# include <saml/SAMLConfig.h>
# include <saml/binding/ArtifactMap.h>
# include <saml/binding/SAMLArtifact.h>
# include <saml/saml1/core/Assertions.h>
const vector<const Handler*>& getAssertionConsumerServicesByBinding(const XMLCh* binding) const;
const Handler* getHandler(const char* path) const;
void getHandlers(vector<const Handler*>& handlers) const;
+ void limitRedirect(const GenericRequest& request, const char* url) const;
void receive(DDF& in, ostream& out) {
// Only current function is to return the headers to clear.
if (m_artifactResolutionDefault) return m_artifactResolutionDefault->getInt("index");
return m_base ? m_base->getArtifactEndpointIndex() : make_pair(false,0);
}
+
+ enum {
+ REDIRECT_LIMIT_INHERIT,
+ REDIRECT_LIMIT_NONE,
+ REDIRECT_LIMIT_EXACT,
+ REDIRECT_LIMIT_HOST,
+ REDIRECT_LIMIT_WHITELIST,
+ REDIRECT_LIMIT_EXACT_WHITELIST,
+ REDIRECT_LIMIT_HOST_WHITELIST
+ } m_redirectLimit;
+ vector<string> m_redirectWhitelist;
};
// Top-level configuration implementation
#ifdef _DEBUG
xmltooling::NDC ndc("XMLApplication");
#endif
- Category& log=Category::getInstance(SHIBSP_LOGCAT".Application");
+ Category& log = Category::getInstance(SHIBSP_LOGCAT".Application");
// First load any property sets.
- load(e, nullptr, this);
+ map<string,string> remapper;
+ remapper["relayStateLimit"] = "redirectLimit";
+ remapper["relayStateWhitelist"] = "redirectWhitelist";
+ load(e, nullptr, this, &remapper);
+
+ // Process redirect limit policy. Do this before assigning the parent pointer
+ // to ensure we get only our Sessions element.
+ const PropertySet* sessionProps = getPropertySet("Sessions");
+ if (sessionProps) {
+ pair<bool,const char*> redirectLimit = sessionProps->getString("redirectLimit");
+ if (redirectLimit.first) {
+ if (!strcmp(redirectLimit.second, "none"))
+ m_redirectLimit = REDIRECT_LIMIT_NONE;
+ else if (!strcmp(redirectLimit.second, "exact"))
+ m_redirectLimit = REDIRECT_LIMIT_EXACT;
+ else if (!strcmp(redirectLimit.second, "host"))
+ m_redirectLimit = REDIRECT_LIMIT_HOST;
+ else {
+ if (!strcmp(redirectLimit.second, "exact+whitelist"))
+ m_redirectLimit = REDIRECT_LIMIT_EXACT_WHITELIST;
+ else if (!strcmp(redirectLimit.second, "exact+host"))
+ m_redirectLimit = REDIRECT_LIMIT_HOST_WHITELIST;
+ else if (!strcmp(redirectLimit.second, "exact+host"))
+ m_redirectLimit = REDIRECT_LIMIT_WHITELIST;
+ else
+ throw ConfigurationException("Unrecognized redirectLimit setting ($1)", params(1, redirectLimit.second));
+ redirectLimit = sessionProps->getString("redirectWhitelist");
+ if (redirectLimit.first) {
+ string dup(redirectLimit.second);
+ split(m_redirectWhitelist, dup, is_space(), algorithm::token_compress_on);
+ }
+ }
+ }
+ else {
+ m_redirectLimit = base ? REDIRECT_LIMIT_INHERIT : REDIRECT_LIMIT_NONE;
+ }
+ }
+ else {
+ m_redirectLimit = base ? REDIRECT_LIMIT_INHERIT : REDIRECT_LIMIT_NONE;
+ }
+
+ // Assign parent.
if (base)
setParent(base);
}
}
+void XMLApplication::limitRedirect(const GenericRequest& request, const char* url) const
+{
+ if (!url || *url == '/')
+ return;
+ if (m_redirectLimit == REDIRECT_LIMIT_INHERIT)
+ return m_base->limitRedirect(request, url);
+ if (m_redirectLimit != REDIRECT_LIMIT_NONE) {
+ vector<string> whitelist;
+ if (m_redirectLimit == REDIRECT_LIMIT_EXACT || m_redirectLimit == REDIRECT_LIMIT_EXACT_WHITELIST) {
+ // Scheme and hostname have to match.
+ if (request.isDefaultPort()) {
+ whitelist.push_back(string(request.getScheme()) + "://" + request.getHostname() + '/');
+ }
+ whitelist.push_back(string(request.getScheme()) + "://" + request.getHostname() + ':' + lexical_cast<string>(request.getPort()) + '/');
+ }
+ else if (m_redirectLimit == REDIRECT_LIMIT_HOST || m_redirectLimit == REDIRECT_LIMIT_HOST_WHITELIST) {
+ // Allow any scheme or port.
+ whitelist.push_back(string("https://") + request.getHostname() + '/');
+ whitelist.push_back(string("http://") + request.getHostname() + '/');
+ whitelist.push_back(string("https://") + request.getHostname() + ':');
+ whitelist.push_back(string("http://") + request.getHostname() + ':');
+ }
+
+ static bool (*startsWithI)(const char*,const char*) = XMLString::startsWithI;
+ if (!whitelist.empty() && find_if(whitelist.begin(), whitelist.end(),
+ boost::bind(startsWithI, url, boost::bind(&string::c_str, _1))) != whitelist.end()) {
+ return;
+ }
+ else if (!m_redirectWhitelist.empty() && find_if(m_redirectWhitelist.begin(), m_redirectWhitelist.end(),
+ boost::bind(startsWithI, url, boost::bind(&string::c_str, _1))) != m_redirectWhitelist.end()) {
+ return;
+ }
+ Category::getInstance(SHIBSP_LOGCAT".Application").warn("redirectLimit policy enforced, blocked redirect to (%s)", url);
+ throw opensaml::SecurityPolicyException("Blocked unacceptable redirect location.");
+ }
+}
+
#ifdef SHIBSP_XERCESC_SHORT_ACCEPTNODE
short
#else
using namespace xmltooling::logging;
using namespace xercesc;
-namespace shibsp {
- void SHIBSP_DLLLOCAL limitRelayState(
- xmltooling::logging::Category& log,
- const Application& application,
- const xmltooling::HTTPRequest& httpRequest,
- const char* relayState
- );
-};
-
#endif /* __shibsp_internal_h__ */