github.com/crowdsecurity/crowdsec@v1.6.1/pkg/appsec/request.go (about)

     1  package appsec
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"regexp"
    13  
    14  	"github.com/google/uuid"
    15  	"github.com/sirupsen/logrus"
    16  	log "github.com/sirupsen/logrus"
    17  )
    18  
    19  const (
    20  	URIHeaderName       = "X-Crowdsec-Appsec-Uri"
    21  	VerbHeaderName      = "X-Crowdsec-Appsec-Verb"
    22  	HostHeaderName      = "X-Crowdsec-Appsec-Host"
    23  	IPHeaderName        = "X-Crowdsec-Appsec-Ip"
    24  	APIKeyHeaderName    = "X-Crowdsec-Appsec-Api-Key"
    25  	UserAgentHeaderName = "X-Crowdsec-Appsec-User-Agent"
    26  )
    27  
    28  type ParsedRequest struct {
    29  	RemoteAddr           string                  `json:"remote_addr,omitempty"`
    30  	Host                 string                  `json:"host,omitempty"`
    31  	ClientIP             string                  `json:"client_ip,omitempty"`
    32  	URI                  string                  `json:"uri,omitempty"`
    33  	Args                 url.Values              `json:"args,omitempty"`
    34  	ClientHost           string                  `json:"client_host,omitempty"`
    35  	Headers              http.Header             `json:"headers,omitempty"`
    36  	URL                  *url.URL                `json:"url,omitempty"`
    37  	Method               string                  `json:"method,omitempty"`
    38  	Proto                string                  `json:"proto,omitempty"`
    39  	Body                 []byte                  `json:"body,omitempty"`
    40  	TransferEncoding     []string                `json:"transfer_encoding,omitempty"`
    41  	UUID                 string                  `json:"uuid,omitempty"`
    42  	Tx                   ExtendedTransaction     `json:"-"`
    43  	ResponseChannel      chan AppsecTempResponse `json:"-"`
    44  	IsInBand             bool                    `json:"-"`
    45  	IsOutBand            bool                    `json:"-"`
    46  	AppsecEngine         string                  `json:"appsec_engine,omitempty"`
    47  	RemoteAddrNormalized string                  `json:"normalized_remote_addr,omitempty"`
    48  	HTTPRequest          *http.Request           `json:"-"`
    49  }
    50  
    51  type ReqDumpFilter struct {
    52  	req                   *ParsedRequest
    53  	HeadersContentFilters []string
    54  	HeadersNameFilters    []string
    55  	HeadersDrop           bool
    56  
    57  	BodyDrop bool
    58  	//BodyContentFilters []string TBD
    59  
    60  	ArgsContentFilters []string
    61  	ArgsNameFilters    []string
    62  	ArgsDrop           bool
    63  }
    64  
    65  func (r *ParsedRequest) DumpRequest(params ...any) *ReqDumpFilter {
    66  	filter := ReqDumpFilter{}
    67  	filter.BodyDrop = true
    68  	filter.HeadersNameFilters = []string{"cookie", "authorization"}
    69  	filter.req = r
    70  	return &filter
    71  }
    72  
    73  // clear filters
    74  func (r *ReqDumpFilter) NoFilters() *ReqDumpFilter {
    75  	r2 := ReqDumpFilter{}
    76  	r2.req = r.req
    77  	return &r2
    78  }
    79  
    80  func (r *ReqDumpFilter) WithEmptyHeadersFilters() *ReqDumpFilter {
    81  	r.HeadersContentFilters = []string{}
    82  	return r
    83  }
    84  
    85  func (r *ReqDumpFilter) WithHeadersContentFilter(filter string) *ReqDumpFilter {
    86  	r.HeadersContentFilters = append(r.HeadersContentFilters, filter)
    87  	return r
    88  }
    89  
    90  func (r *ReqDumpFilter) WithHeadersNameFilter(filter string) *ReqDumpFilter {
    91  	r.HeadersNameFilters = append(r.HeadersNameFilters, filter)
    92  	return r
    93  }
    94  
    95  func (r *ReqDumpFilter) WithNoHeaders() *ReqDumpFilter {
    96  	r.HeadersDrop = true
    97  	return r
    98  }
    99  
   100  func (r *ReqDumpFilter) WithHeaders() *ReqDumpFilter {
   101  	r.HeadersDrop = false
   102  	r.HeadersNameFilters = []string{}
   103  	return r
   104  }
   105  
   106  func (r *ReqDumpFilter) WithBody() *ReqDumpFilter {
   107  	r.BodyDrop = false
   108  	return r
   109  }
   110  
   111  func (r *ReqDumpFilter) WithNoBody() *ReqDumpFilter {
   112  	r.BodyDrop = true
   113  	return r
   114  }
   115  
   116  func (r *ReqDumpFilter) WithEmptyArgsFilters() *ReqDumpFilter {
   117  	r.ArgsContentFilters = []string{}
   118  	return r
   119  }
   120  
   121  func (r *ReqDumpFilter) WithArgsContentFilter(filter string) *ReqDumpFilter {
   122  	r.ArgsContentFilters = append(r.ArgsContentFilters, filter)
   123  	return r
   124  }
   125  
   126  func (r *ReqDumpFilter) WithArgsNameFilter(filter string) *ReqDumpFilter {
   127  	r.ArgsNameFilters = append(r.ArgsNameFilters, filter)
   128  	return r
   129  }
   130  
   131  func (r *ReqDumpFilter) FilterBody(out *ParsedRequest) error {
   132  	if r.BodyDrop {
   133  		return nil
   134  	}
   135  	out.Body = r.req.Body
   136  	return nil
   137  }
   138  
   139  func (r *ReqDumpFilter) FilterArgs(out *ParsedRequest) error {
   140  	if r.ArgsDrop {
   141  		return nil
   142  	}
   143  	if len(r.ArgsContentFilters) == 0 && len(r.ArgsNameFilters) == 0 {
   144  		out.Args = r.req.Args
   145  		return nil
   146  	}
   147  	out.Args = make(url.Values)
   148  	for k, vals := range r.req.Args {
   149  		reject := false
   150  		//exclude by match on name
   151  		for _, filter := range r.ArgsNameFilters {
   152  			ok, err := regexp.MatchString("(?i)"+filter, k)
   153  			if err != nil {
   154  				log.Debugf("error while matching string '%s' with '%s': %s", filter, k, err)
   155  				continue
   156  			}
   157  			if ok {
   158  				reject = true
   159  				break
   160  			}
   161  		}
   162  
   163  		for _, v := range vals {
   164  			//exclude by content
   165  			for _, filter := range r.ArgsContentFilters {
   166  				ok, err := regexp.MatchString("(?i)"+filter, v)
   167  				if err != nil {
   168  					log.Debugf("error while matching string '%s' with '%s': %s", filter, v, err)
   169  					continue
   170  				}
   171  				if ok {
   172  					reject = true
   173  					break
   174  				}
   175  
   176  			}
   177  		}
   178  		//if it was not rejected, let's add it
   179  		if !reject {
   180  			out.Args[k] = vals
   181  		}
   182  	}
   183  	return nil
   184  }
   185  
   186  func (r *ReqDumpFilter) FilterHeaders(out *ParsedRequest) error {
   187  	if r.HeadersDrop {
   188  		return nil
   189  	}
   190  
   191  	if len(r.HeadersContentFilters) == 0 && len(r.HeadersNameFilters) == 0 {
   192  		out.Headers = r.req.Headers
   193  		return nil
   194  	}
   195  
   196  	out.Headers = make(http.Header)
   197  	for k, vals := range r.req.Headers {
   198  		reject := false
   199  		//exclude by match on name
   200  		for _, filter := range r.HeadersNameFilters {
   201  			ok, err := regexp.MatchString("(?i)"+filter, k)
   202  			if err != nil {
   203  				log.Debugf("error while matching string '%s' with '%s': %s", filter, k, err)
   204  				continue
   205  			}
   206  			if ok {
   207  				reject = true
   208  				break
   209  			}
   210  		}
   211  
   212  		for _, v := range vals {
   213  			//exclude by content
   214  			for _, filter := range r.HeadersContentFilters {
   215  				ok, err := regexp.MatchString("(?i)"+filter, v)
   216  				if err != nil {
   217  					log.Debugf("error while matching string '%s' with '%s': %s", filter, v, err)
   218  					continue
   219  				}
   220  				if ok {
   221  					reject = true
   222  					break
   223  				}
   224  
   225  			}
   226  		}
   227  		//if it was not rejected, let's add it
   228  		if !reject {
   229  			out.Headers[k] = vals
   230  		}
   231  	}
   232  	return nil
   233  }
   234  
   235  func (r *ReqDumpFilter) GetFilteredRequest() *ParsedRequest {
   236  	//if there are no filters, we return the original request
   237  	if len(r.HeadersContentFilters) == 0 &&
   238  		len(r.HeadersNameFilters) == 0 &&
   239  		len(r.ArgsContentFilters) == 0 &&
   240  		len(r.ArgsNameFilters) == 0 &&
   241  		!r.BodyDrop && !r.HeadersDrop && !r.ArgsDrop {
   242  		log.Warningf("no filters, returning original request")
   243  		return r.req
   244  	}
   245  
   246  	r2 := ParsedRequest{}
   247  	r.FilterHeaders(&r2)
   248  	r.FilterBody(&r2)
   249  	r.FilterArgs(&r2)
   250  	return &r2
   251  }
   252  
   253  func (r *ReqDumpFilter) ToJSON() error {
   254  	fd, err := os.CreateTemp("", "crowdsec_req_dump_*.json")
   255  	if err != nil {
   256  		return fmt.Errorf("while creating temp file: %w", err)
   257  	}
   258  	defer fd.Close()
   259  	enc := json.NewEncoder(fd)
   260  	enc.SetIndent("", "  ")
   261  
   262  	req := r.GetFilteredRequest()
   263  
   264  	log.Tracef("dumping : %+v", req)
   265  
   266  	if err := enc.Encode(req); err != nil {
   267  		//Don't clobber the temp directory with empty files
   268  		err2 := os.Remove(fd.Name())
   269  		if err2 != nil {
   270  			log.Errorf("while removing temp file %s: %s", fd.Name(), err)
   271  		}
   272  		return fmt.Errorf("while encoding request: %w", err)
   273  	}
   274  	log.Infof("request dumped to %s", fd.Name())
   275  	return nil
   276  }
   277  
   278  // Generate a ParsedRequest from a http.Request. ParsedRequest can be consumed by the App security Engine
   279  func NewParsedRequestFromRequest(r *http.Request, logger *logrus.Entry) (ParsedRequest, error) {
   280  	var err error
   281  	contentLength := r.ContentLength
   282  	if contentLength < 0 {
   283  		contentLength = 0
   284  	}
   285  	body := make([]byte, contentLength)
   286  	if r.Body != nil {
   287  		_, err = io.ReadFull(r.Body, body)
   288  		if err != nil {
   289  			return ParsedRequest{}, fmt.Errorf("unable to read body: %s", err)
   290  		}
   291  		// reset the original body back as it's been read, i'm not sure its needed?
   292  		r.Body = io.NopCloser(bytes.NewBuffer(body))
   293  
   294  	}
   295  	clientIP := r.Header.Get(IPHeaderName)
   296  	if clientIP == "" {
   297  		return ParsedRequest{}, fmt.Errorf("missing '%s' header", IPHeaderName)
   298  	}
   299  
   300  	clientURI := r.Header.Get(URIHeaderName)
   301  	if clientURI == "" {
   302  		return ParsedRequest{}, fmt.Errorf("missing '%s' header", URIHeaderName)
   303  	}
   304  
   305  	clientMethod := r.Header.Get(VerbHeaderName)
   306  	if clientMethod == "" {
   307  		return ParsedRequest{}, fmt.Errorf("missing '%s' header", VerbHeaderName)
   308  	}
   309  
   310  	clientHost := r.Header.Get(HostHeaderName)
   311  	if clientHost == "" { //this might be empty
   312  		logger.Debugf("missing '%s' header", HostHeaderName)
   313  	}
   314  
   315  	userAgent := r.Header.Get(UserAgentHeaderName) //This one is optional
   316  
   317  	// delete those headers before coraza process the request
   318  	delete(r.Header, IPHeaderName)
   319  	delete(r.Header, HostHeaderName)
   320  	delete(r.Header, URIHeaderName)
   321  	delete(r.Header, VerbHeaderName)
   322  	delete(r.Header, UserAgentHeaderName)
   323  	delete(r.Header, APIKeyHeaderName)
   324  
   325  	originalHTTPRequest := r.Clone(r.Context())
   326  	originalHTTPRequest.Body = io.NopCloser(bytes.NewBuffer(body))
   327  	originalHTTPRequest.RemoteAddr = clientIP
   328  	originalHTTPRequest.RequestURI = clientURI
   329  	originalHTTPRequest.Method = clientMethod
   330  	originalHTTPRequest.Host = clientHost
   331  	if userAgent != "" {
   332  		originalHTTPRequest.Header.Set("User-Agent", userAgent)
   333  		r.Header.Set("User-Agent", userAgent) //Override the UA in the original request, as this is what will be used by the waf engine
   334  	} else {
   335  		//If we don't have a forwarded UA, delete the one that was set by the bouncer
   336  		originalHTTPRequest.Header.Del("User-Agent")
   337  	}
   338  
   339  	parsedURL, err := url.Parse(clientURI)
   340  	if err != nil {
   341  		return ParsedRequest{}, fmt.Errorf("unable to parse url '%s': %s", clientURI, err)
   342  	}
   343  
   344  	var remoteAddrNormalized string
   345  	if r.RemoteAddr == "@" {
   346  		r.RemoteAddr = "127.0.0.1:65535"
   347  	}
   348  	// TODO we need to implement forwrded headers
   349  	host, _, err := net.SplitHostPort(r.RemoteAddr)
   350  	if err != nil {
   351  		log.Errorf("Invalid appsec remote IP source %v: %s", r.RemoteAddr, err.Error())
   352  		remoteAddrNormalized = r.RemoteAddr
   353  	} else {
   354  		ip := net.ParseIP(host)
   355  		if ip == nil {
   356  			log.Errorf("Invalid appsec remote IP address source %v", r.RemoteAddr)
   357  			remoteAddrNormalized = r.RemoteAddr
   358  		} else {
   359  			remoteAddrNormalized = ip.String()
   360  		}
   361  	}
   362  
   363  	return ParsedRequest{
   364  		RemoteAddr:           r.RemoteAddr,
   365  		UUID:                 uuid.New().String(),
   366  		ClientHost:           clientHost,
   367  		ClientIP:             clientIP,
   368  		URI:                  clientURI,
   369  		Method:               clientMethod,
   370  		Host:                 clientHost,
   371  		Headers:              r.Header,
   372  		URL:                  parsedURL,
   373  		Proto:                r.Proto,
   374  		Body:                 body,
   375  		Args:                 ParseQuery(parsedURL.RawQuery),
   376  		TransferEncoding:     r.TransferEncoding,
   377  		ResponseChannel:      make(chan AppsecTempResponse),
   378  		RemoteAddrNormalized: remoteAddrNormalized,
   379  		HTTPRequest:          originalHTTPRequest,
   380  	}, nil
   381  }