001package org.apache.archiva.redback.rest.services.interceptors;
002/*
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 */
020
021
022import org.apache.archiva.redback.authentication.AuthenticationResult;
023import org.apache.archiva.redback.authentication.InvalidTokenException;
024import org.apache.archiva.redback.authentication.TokenData;
025import org.apache.archiva.redback.authentication.TokenManager;
026import org.apache.archiva.redback.authorization.RedbackAuthorization;
027import org.apache.archiva.redback.configuration.UserConfiguration;
028import org.apache.archiva.redback.configuration.UserConfigurationKeys;
029import org.apache.archiva.redback.integration.filter.authentication.basic.HttpBasicAuthentication;
030import org.apache.archiva.redback.users.User;
031import org.apache.commons.lang3.StringUtils;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034import org.springframework.stereotype.Service;
035
036import javax.annotation.PostConstruct;
037import javax.annotation.Priority;
038import javax.inject.Inject;
039import javax.inject.Named;
040import javax.servlet.http.HttpServletRequest;
041import javax.ws.rs.container.ContainerRequestContext;
042import javax.ws.rs.container.ContainerRequestFilter;
043import javax.ws.rs.container.ContainerResponseContext;
044import javax.ws.rs.container.ContainerResponseFilter;
045import javax.ws.rs.container.ResourceInfo;
046import javax.ws.rs.core.Context;
047import javax.ws.rs.core.Response;
048import javax.ws.rs.ext.Provider;
049import java.io.IOException;
050import java.net.MalformedURLException;
051import java.net.URL;
052import java.util.ArrayList;
053import java.util.List;
054
055/**
056 * Created by Martin Stockhammer on 19.01.17.
057 * <p>
058 * This interceptor tries to check if requests come from a valid origin and
059 * are not generated by another site on behalf of the real client.
060 * <p>
061 * We are using some of the techniques mentioned in
062 * https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet
063 * <p>
064 * Try to find Origin and Referer of the request.
065 * Match them to the target address, that may be either statically configured or is determined
066 * by the Host/X-Forwarded-For Header.
067 */
068@Provider
069@Service( "requestValidationInterceptor#rest" )
070@Priority( Priorities.PRECHECK )
071public class RequestValidationInterceptor
072    extends AbstractInterceptor
073    implements ContainerRequestFilter, ContainerResponseFilter
074{
075
076
077    private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
078
079    private static final String X_FORWARDED_HOST = "X-Forwarded-Host";
080
081    private static final String X_XSRF_TOKEN = "X-XSRF-TOKEN";
082
083    private static final String ORIGIN = "Origin";
084
085    private static final String REFERER = "Referer";
086
087    private static final int DEFAULT_HTTP = 80;
088
089    private static final int DEFAULT_HTTPS = 443;
090
091    private final Logger log = LoggerFactory.getLogger( getClass() );
092
093    private boolean enabled = true;
094
095    private boolean checkToken = true;
096
097    private boolean useStaticUrl = false;
098
099    private boolean denyAbsentHeaders = true;
100
101    private List<URL> baseUrl = new ArrayList<URL>();
102
103    private HttpServletRequest httpRequest = null;
104
105    @Inject
106    @Named( value = "httpAuthenticator#basic" )
107    private HttpBasicAuthentication httpAuthenticator;
108
109    @Inject
110    @Named( value = "tokenManager#default" )
111    TokenManager tokenManager;
112
113    @Context
114    private ResourceInfo resourceInfo;
115
116    private UserConfiguration config;
117
118    @Override
119    public void filter( ContainerRequestContext requestContext, ContainerResponseContext responseContext ) throws IOException
120    {
121        responseContext.getHeaders().add(
122            "Access-Control-Allow-Origin", "http://localhost:4200");
123        responseContext.getHeaders().add(
124            "Access-Control-Allow-Credentials", "true");
125        responseContext.getHeaders().add(
126            "Access-Control-Allow-Headers",
127            "origin, content-type, accept, authorization");
128        responseContext.getHeaders().add(
129            "Access-Control-Allow-Methods",
130            "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH");
131    }
132
133    private class HeaderValidationInfo
134    {
135
136        final static int UNKNOWN = -1;
137
138        final static int OK = 0;
139
140        final static int F_REFERER_HOST = 1;
141
142        final static int F_REFERER_PORT = 2;
143
144        final static int F_ORIGIN_HOST = 8;
145
146        final static int F_ORIGIN_PORT = 16;
147
148        final static int F_ORIGIN_PROTOCOL = 32;
149
150        boolean headerFound = false;
151
152        URL targetUrl;
153
154        URL originUrl;
155
156        URL refererUrl;
157
158        String targetHost;
159
160        String originHost;
161
162        String refererHost;
163
164        int targetPort;
165
166        int originPort;
167
168        int refererPort;
169
170        int status = UNKNOWN;
171
172        public HeaderValidationInfo( URL targetUrl )
173        {
174            setTargetUrl( targetUrl );
175        }
176
177        public URL getTargetUrl()
178        {
179            return targetUrl;
180        }
181
182        public void setTargetUrl( URL targetUrl )
183        {
184            this.targetUrl = targetUrl;
185            this.targetHost = getHost( targetUrl );
186            this.targetPort = getPort( targetUrl );
187        }
188
189        public URL getOriginUrl()
190        {
191            return originUrl;
192        }
193
194        public void setOriginUrl( URL originUrl )
195        {
196            this.originUrl = originUrl;
197            this.originHost = getHost( originUrl );
198            this.originPort = getPort( originUrl );
199            checkOrigin();
200            this.headerFound = true;
201        }
202
203        public URL getRefererUrl()
204        {
205            return refererUrl;
206        }
207
208        public void setRefererUrl( URL refererUrl )
209        {
210            this.refererUrl = refererUrl;
211            this.refererHost = getHost( refererUrl );
212            this.refererPort = getPort( refererUrl );
213            checkReferer();
214            this.headerFound = true;
215        }
216
217        public String getTargetHost()
218        {
219            return targetHost;
220        }
221
222        public void setTargetHost( String targetHost )
223        {
224            this.targetHost = targetHost;
225        }
226
227        public String getOriginHost()
228        {
229            return originHost;
230        }
231
232        public void setOriginHost( String originHost )
233        {
234            this.originHost = originHost;
235        }
236
237        public String getRefererHost()
238        {
239            return refererHost;
240        }
241
242        public void setRefererHost( String refererHost )
243        {
244            this.refererHost = refererHost;
245        }
246
247        public int getTargetPort()
248        {
249            return targetPort;
250        }
251
252        public void setTargetPort( int targetPort )
253        {
254            this.targetPort = targetPort;
255        }
256
257        public int getOriginPort()
258        {
259            return originPort;
260        }
261
262        public void setOriginPort( int originPort )
263        {
264            this.originPort = originPort;
265        }
266
267        public int getRefererPort()
268        {
269            return refererPort;
270        }
271
272        public void setRefererPort( int refererPort )
273        {
274            this.refererPort = refererPort;
275        }
276
277        public void setStatus( int status )
278        {
279            this.status |= status;
280        }
281
282        public int getStatus()
283        {
284            return this.status;
285        }
286
287        // Origin check for Protocol, Host, Port
288        public void checkOrigin()
289        {
290            if ( this.getStatus() == UNKNOWN )
291            {
292                this.status = OK;
293            }
294            if ( !targetUrl.getProtocol().equals( originUrl.getProtocol() ) )
295            {
296                setStatus( F_ORIGIN_PROTOCOL );
297            }
298            if ( !targetHost.equals( originHost ) )
299            {
300                setStatus( F_ORIGIN_HOST );
301            }
302            if ( targetPort != originPort )
303            {
304                setStatus( F_ORIGIN_PORT );
305            }
306        }
307
308        // Referer check only for Host, Port
309        public void checkReferer()
310        {
311            if ( this.getStatus() == UNKNOWN )
312            {
313                this.status = OK;
314            }
315            if ( !targetHost.equals( refererHost ) )
316            {
317                setStatus( F_REFERER_HOST );
318            }
319            if ( targetPort != refererPort )
320            {
321                setStatus( F_REFERER_PORT );
322            }
323        }
324
325        public boolean hasOriginError()
326        {
327            return ( status & ( F_ORIGIN_PROTOCOL | F_ORIGIN_HOST | F_ORIGIN_PORT ) ) > 0;
328        }
329
330        public boolean hasRefererError()
331        {
332            return ( status & ( F_REFERER_HOST | F_REFERER_PORT ) ) > 0;
333        }
334
335        @Override
336        public String toString()
337        {
338            return "Stat=" + status + ", target=" + targetUrl + ", origin=" + originUrl + ", referer=" + refererUrl;
339        }
340    }
341
342    @Inject
343    public RequestValidationInterceptor( @Named( value = "userConfiguration#default" ) UserConfiguration config )
344    {
345        this.config = config;
346    }
347
348    @PostConstruct
349    public void init()
350    {
351        List<String> baseUrlList = config.getList( UserConfigurationKeys.REST_BASE_URL );
352        if ( baseUrlList != null )
353        {
354            for ( String baseUrlStr : baseUrlList )
355            {
356                if ( !"".equals( baseUrlStr.trim() ) )
357                {
358                    try
359                    {
360                        baseUrl.add( new URL( baseUrlStr ) );
361                        useStaticUrl = true;
362                    }
363                    catch ( MalformedURLException ex )
364                    {
365                        log.error( "Configured baseUrl (rest.baseUrl={}) is invalid. Message: {}", baseUrlStr,
366                            ex.getMessage() );
367                    }
368                }
369            }
370        }
371        denyAbsentHeaders = config.getBoolean( UserConfigurationKeys.REST_CSRF_ABSENTORIGIN_DENY, true );
372        enabled = config.getBoolean( UserConfigurationKeys.REST_CSRF_ENABLED, true );
373        if ( !enabled )
374        {
375            log.info( "CSRF Filter is disabled by configuration" );
376        }
377        else
378        {
379            log.info( "CSRF Filter is enable" );
380        }
381        checkToken = !config.getBoolean( UserConfigurationKeys.REST_CSRF_DISABLE_TOKEN_VALIDATION, false );
382        if ( !checkToken )
383        {
384            log.info( "CSRF Token validation is disabled by configuration" );
385        }
386        else
387        {
388            log.info( "CSRF Token validation is enable" );
389        }
390    }
391
392    @Override
393    public void filter( ContainerRequestContext containerRequestContext )
394        throws IOException
395    {
396
397        if ( enabled )
398        {
399
400            final String requestPath = containerRequestContext.getUriInfo( ).getPath( );
401            if (ignoreAuth( requestPath )) {
402                return;
403            }
404
405            HttpServletRequest request = getRequest();
406            List<URL> targetUrls = getTargetUrl( request );
407            if ( targetUrls == null )
408            {
409                log.error( "Could not verify target URL." );
410                containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() );
411                return;
412            }
413            List<HeaderValidationInfo> validationInfos = new ArrayList<HeaderValidationInfo>();
414            boolean targetMatch = false;
415            boolean noHeader = true;
416            for ( URL targetUrl : targetUrls )
417            {
418                log.trace( "Checking against target URL: {}", targetUrl );
419                HeaderValidationInfo info = checkSourceRequestHeader( new HeaderValidationInfo( targetUrl ), request );
420                // We need only one match
421                noHeader = noHeader && info.getStatus() == info.UNKNOWN;
422                if ( info.getStatus() == info.OK )
423                {
424                    targetMatch = true;
425                    break;
426                }
427                else
428                {
429                    validationInfos.add( info );
430                }
431            }
432            if ( noHeader && denyAbsentHeaders )
433            {
434                log.warn( "Request denied. No Origin or Referer header found and {}=true",
435                    UserConfigurationKeys.REST_CSRF_ABSENTORIGIN_DENY );
436                containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() );
437                return;
438            }
439            if ( !targetMatch )
440            {
441                log.warn( "HTTP Header check failed. Assuming CSRF attack." );
442                for ( HeaderValidationInfo info : validationInfos )
443                {
444                    if ( info.hasOriginError() )
445                    {
446                        log.warn(
447                            "Origin Header does not match: originUrl={}, targetUrl={}. Matches: Host={}, Port={}, Protocol={}",
448                            info.originUrl, info.targetUrl, ( info.getStatus() & info.F_ORIGIN_HOST ) == 0,
449                            ( info.getStatus() & info.F_ORIGIN_PORT ) == 0,
450                            ( info.getStatus() & info.F_ORIGIN_PROTOCOL ) == 0 );
451                    }
452                    if ( info.hasRefererError() )
453                    {
454                        log.warn(
455                            "Referer Header does not match: refererUrl={}, targetUrl={}. Matches: Host={}, Port={}",
456                            info.refererUrl, info.targetUrl, ( info.getStatus() & info.F_REFERER_HOST ) == 0,
457                            ( info.getStatus() & info.F_REFERER_PORT ) == 0 );
458                    }
459                }
460                containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() );
461                return;
462            }
463            if ( checkToken )
464            {
465                checkValidationToken( containerRequestContext, request );
466            }
467        }
468    }
469
470    /**
471     * Checks the request for a validation token header. It takes the encrypted token, decrypts it
472     * and compares the user information from the token to the logged in user.
473     *
474     * @param containerRequestContext
475     * @param request
476     */
477    private void checkValidationToken( ContainerRequestContext containerRequestContext, HttpServletRequest request )
478    {
479        RedbackAuthorization redbackAuthorization = getRedbackAuthorization( resourceInfo );
480        // We check only services that are restricted
481        if ( !redbackAuthorization.noRestriction() )
482        {
483            String tokenString = request.getHeader( X_XSRF_TOKEN );
484            if ( tokenString == null || tokenString.length() == 0 )
485            {
486                log.warn( "No validation token header found: {}", X_XSRF_TOKEN );
487                containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() );
488                return;
489            }
490
491            try
492            {
493                TokenData td = tokenManager.decryptToken( tokenString );
494                AuthenticationResult auth = getAuthenticationResult( containerRequestContext, httpAuthenticator, request );
495                if ( auth == null )
496                {
497                    log.error( "Not authentication data found" );
498                    containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() );
499                    return;
500                }
501                User loggedIn = auth.getUser();
502                if ( loggedIn == null )
503                {
504                    log.error( "User not logged in" );
505                    containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() );
506                    return;
507                }
508                String username = loggedIn.getUsername();
509                if ( !td.isValid() || !td.getUser().equals( username ) )
510                {
511                    log.error( "Invalid data in validation token header {} for user {}: isValid={}, username={}",
512                        X_XSRF_TOKEN, username, td.isValid(), td.getUser() );
513                    containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() );
514                }
515            }
516            catch ( InvalidTokenException e )
517            {
518                log.error( "Token validation failed {}", e.getMessage() );
519                containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() );
520            }
521        }
522        log.debug( "Token validated" );
523    }
524
525    private HttpServletRequest getRequest()
526    {
527        if ( httpRequest != null )
528        {
529            return httpRequest;
530        }
531        else
532        {
533            return getHttpServletRequest( );
534        }
535    }
536
537    private List<URL> getTargetUrl( HttpServletRequest request )
538    {
539        if ( useStaticUrl )
540        {
541            return baseUrl;
542        }
543        else
544        {
545            List<URL> urls = new ArrayList<URL>();
546            URL requestUrl;
547            try
548            {
549                requestUrl = new URL( request.getRequestURL().toString() );
550                urls.add( requestUrl );
551            }
552            catch ( MalformedURLException ex )
553            {
554                log.error( "Bad Request URL {}, Message: {}", request.getRequestURL(), ex.getMessage() );
555                return null;
556            }
557            String xforwarded = request.getHeader( X_FORWARDED_HOST );
558            String xforwardedProto = request.getHeader( X_FORWARDED_PROTO );
559            if ( xforwardedProto == null )
560            {
561                xforwardedProto = requestUrl.getProtocol();
562            }
563
564            if ( xforwarded != null && !StringUtils.isEmpty( xforwarded ) )
565            {
566                // X-Forwarded-Host header may contain multiple hosts if there is
567                // more than one proxy between the client and the server
568                String[] forwardedList = xforwarded.split( "\\s*,\\s*" );
569                for ( String hostname : forwardedList )
570                {
571                    try
572                    {
573                        urls.add( new URL( xforwardedProto + "://" + hostname ) );
574                    }
575                    catch ( MalformedURLException ex )
576                    {
577                        log.warn( "X-Forwarded-Host Header is malformed: {}", ex.getMessage() );
578                    }
579                }
580            }
581            return urls;
582        }
583    }
584
585    private int getPort( final URL url )
586    {
587        return url.getPort() > 0
588            ? url.getPort()
589            : ( "https".equals( url.getProtocol() ) ? DEFAULT_HTTPS : DEFAULT_HTTP );
590    }
591
592    private String getHost( final URL url )
593    {
594        return url.getHost().trim().toLowerCase();
595    }
596
597    /**
598     * Checks the validation headers. First the Origin header is checked, if this fails
599     * or is absent, the referer header is checked.
600     *
601     * @param info    The info object that must be populated with the targetURL
602     * @param request The HTTP request object
603     * @return A info object with updated status information
604     */
605    private HeaderValidationInfo checkSourceRequestHeader( final HeaderValidationInfo info,
606                                                           final HttpServletRequest request )
607    {
608        String origin = request.getHeader( ORIGIN );
609        if ( origin != null )
610        {
611            try
612            {
613                info.setOriginUrl( new URL( origin ) );
614            }
615            catch ( MalformedURLException e )
616            {
617                log.warn( "Bad origin header found: {}", origin );
618            }
619        }
620        // Check referer if Origin header dos not match or is not available
621        if ( info.getStatus() != info.OK )
622        {
623            String referer = request.getHeader( REFERER );
624            if ( referer != null )
625            {
626                try
627                {
628                    info.setRefererUrl( new URL( referer ) );
629                }
630                catch ( MalformedURLException ex )
631                {
632                    log.warn( "Bad URL in Referer HTTP-Header: {}, Message: {}", referer, ex.getMessage() );
633                }
634            }
635        }
636        return info;
637    }
638
639    public void setHttpRequest( HttpServletRequest request )
640    {
641        this.httpRequest = request;
642    }
643
644}