/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM Project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package password.pwm.http.filter; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.PwmApplicationMode; import password.pwm.PwmConstants; import password.pwm.bean.LocalSessionStateBean; import password.pwm.bean.LoginInfoBean; import password.pwm.bean.SessionLabel; import password.pwm.config.Configuration; import password.pwm.config.PwmSetting; import password.pwm.config.option.SessionVerificationMode; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmOperationalException; import password.pwm.error.PwmUnrecoverableException; import password.pwm.http.HttpHeader; import password.pwm.http.JspUrl; import password.pwm.http.ProcessStatus; import password.pwm.http.PwmHttpRequestWrapper; import password.pwm.http.PwmHttpResponseWrapper; import password.pwm.http.PwmRequest; import password.pwm.http.PwmResponse; import password.pwm.http.PwmSession; import password.pwm.http.PwmURL; import password.pwm.svc.stats.Statistic; import password.pwm.util.java.StringUtil; import password.pwm.util.java.TimeDuration; import password.pwm.util.logging.PwmLogger; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.net.InetAddress; import java.net.URI; import java.net.UnknownHostException; import java.time.Instant; import java.util.Arrays; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; /** * This session filter (invoked by the container through the web.xml descriptor) wraps all calls to the * servlets in the container. * <p/> * It is responsible for managing some aspects of the user session and also for enforcing security * functionality such as intruder lockout. * * @author Jason D. Rivard */ public class SessionFilter extends AbstractPwmFilter { private static final PwmLogger LOGGER = PwmLogger.forClass(SessionFilter.class); @Override boolean isInterested(final PwmApplicationMode mode, final PwmURL pwmURL) { return true; } public void processFilter( final PwmApplicationMode mode, final PwmRequest pwmRequest, final PwmFilterChain chain ) throws IOException, ServletException, PwmUnrecoverableException { final PwmURL pwmURL = pwmRequest.getURL(); if (!pwmURL.isWebServiceURL() && !pwmURL.isResourceURL()) { if (handleStandardRequestOperations(pwmRequest) == ProcessStatus.Halt) { return; } } try { chain.doFilter(); } catch (IOException e) { LOGGER.trace(pwmRequest.getPwmSession(), "IO exception during servlet processing: " + e.getMessage()); throw new ServletException(e); } catch (Throwable e) { if (e instanceof ServletException && e.getCause() != null && e.getCause() instanceof NoClassDefFoundError && e.getCause().getMessage() != null && e.getCause().getMessage().contains("JaxbAnnotationIntrospector") ) { LOGGER.debug("ignoring JaxbAnnotationIntrospector NoClassDefFoundError: " + e.getMessage()); // this is a jersey 1.18 bug that occurs once per execution } else { LOGGER.warn(pwmRequest.getPwmSession(), "unhandled exception " + e.getMessage(), e); } throw new ServletException(e); } } private ProcessStatus handleStandardRequestOperations( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ServletException { final PwmApplication pwmApplication = pwmRequest.getPwmApplication(); final Configuration config = pwmRequest.getConfig(); final PwmSession pwmSession = pwmRequest.getPwmSession(); final LocalSessionStateBean ssBean = pwmSession.getSessionStateBean(); final PwmResponse resp = pwmRequest.getPwmResponse(); // debug the http session headers if (!pwmSession.getSessionStateBean().isDebugInitialized()) { LOGGER.trace(pwmSession, pwmRequest.debugHttpHeaders()); pwmSession.getSessionStateBean().setDebugInitialized(true); } // output request information to debug log pwmRequest.debugHttpRequestToLog(); try { pwmApplication.getSessionStateService().readLoginSessionState(pwmRequest); } catch (PwmUnrecoverableException e) { LOGGER.warn(pwmRequest, "error while reading login session state: " + e.getMessage()); } // mark last url if (!new PwmURL(pwmRequest.getHttpServletRequest()).isCommandServletURL()) { ssBean.setLastRequestURL(pwmRequest.getHttpServletRequest().getRequestURI()); } // mark last request time. ssBean.setSessionLastAccessedTime(Instant.now()); // check the page leave notice if (checkPageLeaveNotice(pwmSession, config)) { LOGGER.warn("invalidating session due to dirty page leave time greater then configured timeout"); pwmRequest.invalidateSession(); resp.sendRedirect(pwmRequest.getHttpServletRequest().getRequestURI()); return ProcessStatus.Halt; } //override session locale due to parameter handleLocaleParam(pwmRequest); //set the session's theme handleThemeParam(pwmRequest); //check the sso override flag handleSsoOverrideParam(pwmRequest); //check for session verification failure if (!ssBean.isSessionVerified()) { // ignore resource requests final SessionVerificationMode mode = config.readSettingAsEnum(PwmSetting.ENABLE_SESSION_VERIFICATION, SessionVerificationMode.class); if (mode == SessionVerificationMode.OFF) { ssBean.setSessionVerified(true); } else { if (verifySession(pwmRequest, mode) == ProcessStatus.Halt) { return ProcessStatus.Halt; } } } { final String forwardURLParamName = config.readAppProperty(AppProperty.HTTP_PARAM_NAME_FORWARD_URL); final String forwardURL = pwmRequest.readParameterAsString(forwardURLParamName); if (forwardURL != null && forwardURL.length() > 0) { try { checkUrlAgainstWhitelist(pwmApplication, pwmRequest.getSessionLabel(), forwardURL); } catch (PwmOperationalException e) { LOGGER.error(pwmRequest, e.getErrorInformation()); pwmRequest.respondWithError(e.getErrorInformation()); return ProcessStatus.Halt; } ssBean.setForwardURL(forwardURL); LOGGER.debug(pwmRequest, "forwardURL parameter detected in request, setting session forward url to " + forwardURL); } } { final String logoutURLParamName = config.readAppProperty(AppProperty.HTTP_PARAM_NAME_LOGOUT_URL); final String logoutURL = pwmRequest.readParameterAsString(logoutURLParamName); if (logoutURL != null && logoutURL.length() > 0) { try { checkUrlAgainstWhitelist(pwmApplication, pwmRequest.getSessionLabel(), logoutURL); } catch (PwmOperationalException e) { LOGGER.error(pwmRequest, e.getErrorInformation()); pwmRequest.respondWithError(e.getErrorInformation()); return ProcessStatus.Halt; } ssBean.setLogoutURL(logoutURL); LOGGER.debug(pwmRequest, "logoutURL parameter detected in request, setting session logout url to " + logoutURL); } } if ("true".equalsIgnoreCase(pwmRequest.readParameterAsString( pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_PARAM_NAME_PASSWORD_EXPIRED)))) { pwmSession.getUserInfoBean().getPasswordState().setExpired(true); } // update last request time. ssBean.setSessionLastAccessedTime(Instant.now()); if (pwmApplication.getStatisticsManager() != null) { pwmApplication.getStatisticsManager().incrementValue(Statistic.HTTP_REQUESTS); } return ProcessStatus.Continue; } public void destroy() { } /** * Attempt to determine if user agent is able to track sessions (either via url rewriting or cookies). */ private static ProcessStatus verifySession( final PwmRequest pwmRequest, final SessionVerificationMode mode ) throws IOException, ServletException, PwmUnrecoverableException { final LocalSessionStateBean ssBean = pwmRequest.getPwmSession().getSessionStateBean(); final HttpServletRequest req = pwmRequest.getHttpServletRequest(); final PwmResponse pwmResponse = pwmRequest.getPwmResponse(); if (!pwmRequest.getMethod().isIdempotent() && pwmRequest.hasParameter(PwmConstants.PARAM_FORM_ID)) { LOGGER.debug(pwmRequest,"session is unvalidated but can not be validated during a " + pwmRequest.getMethod().toString() + " request, will allow"); return ProcessStatus.Continue; } { final String acceptEncodingHeader = pwmRequest.getHttpServletRequest().getHeader(HttpHeader.Accept.getHttpName()); if (acceptEncodingHeader != null && acceptEncodingHeader.contains("json")) { LOGGER.debug(pwmRequest, "session is unvalidated but can not be validated during a json request, will allow"); return ProcessStatus.Continue; } } if (pwmRequest.getURL().isCommandServletURL()) { return ProcessStatus.Continue; } final String verificationParamName = pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_PARAM_SESSION_VERIFICATION); final String keyFromRequest = pwmRequest.readParameterAsString(verificationParamName, PwmHttpRequestWrapper.Flag.BypassValidation); // request doesn't have key, so make a new one, store it in the session, and redirect back here with the new key. if (keyFromRequest == null || keyFromRequest.length() < 1) { final String returnURL = figureValidationURL(pwmRequest, ssBean.getSessionVerificationKey()); LOGGER.trace(pwmRequest, "session has not been validated, redirecting with verification key to " + returnURL); pwmResponse.setHeader(HttpHeader.Connection, "close"); // better chance of detecting un-sticky sessions this way if (mode == SessionVerificationMode.VERIFY_AND_CACHE) { req.setAttribute("Location", returnURL); pwmResponse.forwardToJsp(JspUrl.INIT); } else { pwmResponse.sendRedirect(returnURL); } return ProcessStatus.Halt; } // else, request has a key, so investigate. if (keyFromRequest.equals(ssBean.getSessionVerificationKey())) { final String returnURL = figureValidationURL(pwmRequest, null); // session looks, good, mark it as such and return; LOGGER.trace(pwmRequest, "session validated, redirecting to original request url: " + returnURL); ssBean.setSessionVerified(true); pwmRequest.getPwmResponse().sendRedirect(returnURL); return ProcessStatus.Halt; } // user's session is messed up. send to error page. final String errorMsg = "client unable to reply with session key"; final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_BAD_SESSION, errorMsg); LOGGER.error(pwmRequest, errorInformation); pwmRequest.respondWithError(errorInformation, true); return ProcessStatus.Halt; } private static String figureValidationURL(final PwmRequest pwmRequest, final String validationKey) { final HttpServletRequest req = pwmRequest.getHttpServletRequest(); final StringBuilder sb = new StringBuilder(); sb.append(req.getRequestURL()); final String verificationParamName = pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_PARAM_SESSION_VERIFICATION); for (final Enumeration paramEnum = req.getParameterNames(); paramEnum.hasMoreElements(); ) { final String paramName = (String) paramEnum.nextElement(); // check to make sure param is in query string if (req.getQueryString() != null && req.getQueryString().contains(StringUtil.urlDecode(paramName))) { if (!verificationParamName.equals(paramName)) { final List<String> paramValues = Arrays.asList(req.getParameterValues(paramName)); for (final Iterator<String> valueIter = paramValues.iterator(); valueIter.hasNext(); ) { final String value = valueIter.next(); sb.append(sb.toString().contains("?") ? "&" : "?"); sb.append(StringUtil.urlEncode(paramName)).append("="); sb.append(StringUtil.urlEncode(value)); } } } else { LOGGER.debug("dropping non-query string (body?) parameter '" + paramName + "' during redirect validation)"); } } if (validationKey != null) { sb.append(sb.toString().contains("?") ? "&" : "?"); sb.append(verificationParamName).append("=").append(validationKey); } return sb.toString(); } private static boolean checkPageLeaveNotice(final PwmSession pwmSession, final Configuration config) { final long configuredSeconds = config.readSettingAsLong(PwmSetting.SECURITY_PAGE_LEAVE_NOTICE_TIMEOUT); if (configuredSeconds <= 0) { return false; } final Instant currentPageLeaveNotice = pwmSession.getSessionStateBean().getPageLeaveNoticeTime(); pwmSession.getSessionStateBean().setPageLeaveNoticeTime(null); if (currentPageLeaveNotice == null) { return false; } if (TimeDuration.fromCurrent(currentPageLeaveNotice).getTotalSeconds() <= configuredSeconds) { return false; } return true; } private static void handleLocaleParam( final PwmRequest pwmRequest ) throws PwmUnrecoverableException { final Configuration config = pwmRequest.getConfig(); final String localeParamName = config.readAppProperty(AppProperty.HTTP_PARAM_NAME_LOCALE); final String localeCookieName = config.readAppProperty(AppProperty.HTTP_COOKIE_LOCALE_NAME); final String requestedLocale = pwmRequest.readParameterAsString(localeParamName); final int cookieAgeSeconds = (int) pwmRequest.getConfig().readSettingAsLong(PwmSetting.LOCALE_COOKIE_MAX_AGE); if (requestedLocale != null && requestedLocale.length() > 0) { LOGGER.debug(pwmRequest, "detected locale request parameter " + localeParamName + " with value " + requestedLocale); if (pwmRequest.getPwmSession().setLocale(pwmRequest.getPwmApplication(), requestedLocale)) { if (cookieAgeSeconds > 0) { pwmRequest.getPwmResponse().writeCookie( localeCookieName, requestedLocale, cookieAgeSeconds, PwmHttpResponseWrapper.CookiePath.Application ); } } } } private static void handleThemeParam( final PwmRequest pwmRequest ) throws PwmUnrecoverableException { final Configuration config = pwmRequest.getConfig(); final String themeParameterName = config.readAppProperty(AppProperty.HTTP_PARAM_NAME_THEME); final String themeReqParameter = pwmRequest.readParameterAsString(themeParameterName); if (themeReqParameter != null && !themeReqParameter.isEmpty()) { if (pwmRequest.getPwmApplication().getResourceServletService().checkIfThemeExists(pwmRequest, themeReqParameter)) { pwmRequest.getPwmSession().getSessionStateBean().setTheme(themeReqParameter); final String themeCookieName = config.readAppProperty(AppProperty.HTTP_COOKIE_THEME_NAME); if (themeCookieName != null && themeCookieName.length() > 0) { final String configuredTheme = config.readSettingAsString(PwmSetting.INTERFACE_THEME); if (configuredTheme != null && configuredTheme.equalsIgnoreCase(themeReqParameter)) { pwmRequest.getPwmResponse().removeCookie(themeCookieName, PwmHttpResponseWrapper.CookiePath.Application); } else { final int maxAge = Integer.parseInt(config.readAppProperty(AppProperty.HTTP_COOKIE_THEME_AGE)); pwmRequest.getPwmResponse().writeCookie(themeCookieName, themeReqParameter, maxAge, PwmHttpResponseWrapper.CookiePath.Application); } } } } } private static void handleSsoOverrideParam( final PwmRequest pwmRequest ) throws PwmUnrecoverableException { final String ssoOverrideParameterName = pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_PARAM_NAME_SSO_OVERRIDE); if (pwmRequest.hasParameter(ssoOverrideParameterName)) { if (pwmRequest.readParameterAsBoolean(ssoOverrideParameterName)) { LOGGER.trace(pwmRequest, "enabling sso authentication due to parameter " + ssoOverrideParameterName + "=" + pwmRequest.readParameterAsString(ssoOverrideParameterName)); pwmRequest.getPwmSession().getLoginInfoBean().removeFlag(LoginInfoBean.LoginFlag.noSso); } else { LOGGER.trace(pwmRequest, "disabling sso authentication due to parameter " + ssoOverrideParameterName + "=" + pwmRequest.readParameterAsString(ssoOverrideParameterName)); pwmRequest.getPwmSession().getLoginInfoBean().setFlag(LoginInfoBean.LoginFlag.noSso); } } } private static void checkUrlAgainstWhitelist( final PwmApplication pwmApplication, final SessionLabel sessionLabel, final String inputURL ) throws PwmOperationalException { LOGGER.trace(sessionLabel, "beginning test of requested redirect URL: " + inputURL); if (inputURL == null || inputURL.isEmpty()) { return; } final URI inputURI; try { inputURI = URI.create(inputURL); } catch (IllegalArgumentException e) { LOGGER.error(sessionLabel, "unable to parse requested redirect url '" + inputURL + "', error: " + e.getMessage()); // dont put input uri in error response final String errorMsg = "unable to parse url: " + e.getMessage(); throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_REDIRECT_ILLEGAL,errorMsg)); } { // check to make sure we werent handed a non-http uri. final String scheme = inputURI.getScheme(); if (scheme != null && !scheme.isEmpty() && !"http".equalsIgnoreCase(scheme) && !"https".equals(scheme)) { final String errorMsg = "unsupported url scheme"; throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_REDIRECT_ILLEGAL,errorMsg)); } } if (inputURI.getHost() != null && !inputURI.getHost().isEmpty()) { // disallow localhost uri try { final InetAddress inetAddress = InetAddress.getByName(inputURI.getHost()); if (inetAddress.isLoopbackAddress()) { final String errorMsg = "redirect to loopback host is not permitted"; throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_REDIRECT_ILLEGAL,errorMsg)); } } catch (UnknownHostException e) { /* noop */ } } final StringBuilder sb = new StringBuilder(); if (inputURI.getScheme() != null) { sb.append(inputURI.getScheme()); sb.append("://"); } if (inputURI.getHost() != null) { sb.append(inputURI.getHost()); } if (inputURI.getPort() != -1) { sb.append(":"); sb.append(inputURI.getPort()); } if (inputURI.getPath() != null) { sb.append(inputURI.getPath()); } final String testURI = sb.toString(); LOGGER.trace(sessionLabel, "preparing to whitelist test parsed and decoded URL: " + testURI); final String REGEX_PREFIX = "regex:"; final List<String> whiteList = pwmApplication.getConfig().readSettingAsStringArray(PwmSetting.SECURITY_REDIRECT_WHITELIST); for (final String loopFragment : whiteList) { if (loopFragment.startsWith(REGEX_PREFIX)) { try { final String strPattern = loopFragment.substring(REGEX_PREFIX.length(), loopFragment.length()); final Pattern pattern = Pattern.compile(strPattern); if (pattern.matcher(testURI).matches()) { LOGGER.debug(sessionLabel, "positive URL match for regex pattern: " + strPattern); return; } else { LOGGER.trace(sessionLabel, "negative URL match for regex pattern: " + strPattern); } } catch (Exception e) { LOGGER.error(sessionLabel, "error while testing URL match for regex pattern: '" + loopFragment + "', error: " + e.getMessage()); } } else { if (testURI.startsWith(loopFragment)) { LOGGER.debug(sessionLabel, "positive URL match for pattern: " + loopFragment); return; } else { LOGGER.trace(sessionLabel, "negative URL match for pattern: " + loopFragment); } } } final String errorMsg = testURI + " is not a match for any configured redirect whitelist, see setting: " + PwmSetting.SECURITY_REDIRECT_WHITELIST.toMenuLocationDebug(null,PwmConstants.DEFAULT_LOCALE); throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_REDIRECT_ILLEGAL,errorMsg)); } }