123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- package appsec
- import (
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "net"
- "net/http"
- "net/url"
- "os"
- "regexp"
- "github.com/google/uuid"
- "github.com/sirupsen/logrus"
- log "github.com/sirupsen/logrus"
- )
- const (
- URIHeaderName = "X-Crowdsec-Appsec-Uri"
- VerbHeaderName = "X-Crowdsec-Appsec-Verb"
- HostHeaderName = "X-Crowdsec-Appsec-Host"
- IPHeaderName = "X-Crowdsec-Appsec-Ip"
- APIKeyHeaderName = "X-Crowdsec-Appsec-Api-Key"
- )
- type ParsedRequest struct {
- RemoteAddr string `json:"remote_addr,omitempty"`
- Host string `json:"host,omitempty"`
- ClientIP string `json:"client_ip,omitempty"`
- URI string `json:"uri,omitempty"`
- Args url.Values `json:"args,omitempty"`
- ClientHost string `json:"client_host,omitempty"`
- Headers http.Header `json:"headers,omitempty"`
- URL *url.URL `json:"url,omitempty"`
- Method string `json:"method,omitempty"`
- Proto string `json:"proto,omitempty"`
- Body []byte `json:"body,omitempty"`
- TransferEncoding []string `json:"transfer_encoding,omitempty"`
- UUID string `json:"uuid,omitempty"`
- Tx ExtendedTransaction `json:"-"`
- ResponseChannel chan AppsecTempResponse `json:"-"`
- IsInBand bool `json:"-"`
- IsOutBand bool `json:"-"`
- AppsecEngine string `json:"appsec_engine,omitempty"`
- RemoteAddrNormalized string `json:"normalized_remote_addr,omitempty"`
- HTTPRequest *http.Request `json:"-"`
- }
- type ReqDumpFilter struct {
- req *ParsedRequest
- HeadersContentFilters []string
- HeadersNameFilters []string
- HeadersDrop bool
- BodyDrop bool
- //BodyContentFilters []string TBD
- ArgsContentFilters []string
- ArgsNameFilters []string
- ArgsDrop bool
- }
- func (r *ParsedRequest) DumpRequest(params ...any) *ReqDumpFilter {
- filter := ReqDumpFilter{}
- filter.BodyDrop = true
- filter.HeadersNameFilters = []string{"cookie", "authorization"}
- filter.req = r
- return &filter
- }
- // clear filters
- func (r *ReqDumpFilter) NoFilters() *ReqDumpFilter {
- r2 := ReqDumpFilter{}
- r2.req = r.req
- return &r2
- }
- func (r *ReqDumpFilter) WithEmptyHeadersFilters() *ReqDumpFilter {
- r.HeadersContentFilters = []string{}
- return r
- }
- func (r *ReqDumpFilter) WithHeadersContentFilter(filter string) *ReqDumpFilter {
- r.HeadersContentFilters = append(r.HeadersContentFilters, filter)
- return r
- }
- func (r *ReqDumpFilter) WithHeadersNameFilter(filter string) *ReqDumpFilter {
- r.HeadersNameFilters = append(r.HeadersNameFilters, filter)
- return r
- }
- func (r *ReqDumpFilter) WithNoHeaders() *ReqDumpFilter {
- r.HeadersDrop = true
- return r
- }
- func (r *ReqDumpFilter) WithHeaders() *ReqDumpFilter {
- r.HeadersDrop = false
- r.HeadersNameFilters = []string{}
- return r
- }
- func (r *ReqDumpFilter) WithBody() *ReqDumpFilter {
- r.BodyDrop = false
- return r
- }
- func (r *ReqDumpFilter) WithNoBody() *ReqDumpFilter {
- r.BodyDrop = true
- return r
- }
- func (r *ReqDumpFilter) WithEmptyArgsFilters() *ReqDumpFilter {
- r.ArgsContentFilters = []string{}
- return r
- }
- func (r *ReqDumpFilter) WithArgsContentFilter(filter string) *ReqDumpFilter {
- r.ArgsContentFilters = append(r.ArgsContentFilters, filter)
- return r
- }
- func (r *ReqDumpFilter) WithArgsNameFilter(filter string) *ReqDumpFilter {
- r.ArgsNameFilters = append(r.ArgsNameFilters, filter)
- return r
- }
- func (r *ReqDumpFilter) FilterBody(out *ParsedRequest) error {
- if r.BodyDrop {
- return nil
- }
- out.Body = r.req.Body
- return nil
- }
- func (r *ReqDumpFilter) FilterArgs(out *ParsedRequest) error {
- if r.ArgsDrop {
- return nil
- }
- if len(r.ArgsContentFilters) == 0 && len(r.ArgsNameFilters) == 0 {
- out.Args = r.req.Args
- return nil
- }
- out.Args = make(url.Values)
- for k, vals := range r.req.Args {
- reject := false
- //exclude by match on name
- for _, filter := range r.ArgsNameFilters {
- ok, err := regexp.MatchString("(?i)"+filter, k)
- if err != nil {
- log.Debugf("error while matching string '%s' with '%s': %s", filter, k, err)
- continue
- }
- if ok {
- reject = true
- break
- }
- }
- for _, v := range vals {
- //exclude by content
- for _, filter := range r.ArgsContentFilters {
- ok, err := regexp.MatchString("(?i)"+filter, v)
- if err != nil {
- log.Debugf("error while matching string '%s' with '%s': %s", filter, v, err)
- continue
- }
- if ok {
- reject = true
- break
- }
- }
- }
- //if it was not rejected, let's add it
- if !reject {
- out.Args[k] = vals
- }
- }
- return nil
- }
- func (r *ReqDumpFilter) FilterHeaders(out *ParsedRequest) error {
- if r.HeadersDrop {
- return nil
- }
- if len(r.HeadersContentFilters) == 0 && len(r.HeadersNameFilters) == 0 {
- out.Headers = r.req.Headers
- return nil
- }
- out.Headers = make(http.Header)
- for k, vals := range r.req.Headers {
- reject := false
- //exclude by match on name
- for _, filter := range r.HeadersNameFilters {
- ok, err := regexp.MatchString("(?i)"+filter, k)
- if err != nil {
- log.Debugf("error while matching string '%s' with '%s': %s", filter, k, err)
- continue
- }
- if ok {
- reject = true
- break
- }
- }
- for _, v := range vals {
- //exclude by content
- for _, filter := range r.HeadersContentFilters {
- ok, err := regexp.MatchString("(?i)"+filter, v)
- if err != nil {
- log.Debugf("error while matching string '%s' with '%s': %s", filter, v, err)
- continue
- }
- if ok {
- reject = true
- break
- }
- }
- }
- //if it was not rejected, let's add it
- if !reject {
- out.Headers[k] = vals
- }
- }
- return nil
- }
- func (r *ReqDumpFilter) GetFilteredRequest() *ParsedRequest {
- //if there are no filters, we return the original request
- if len(r.HeadersContentFilters) == 0 &&
- len(r.HeadersNameFilters) == 0 &&
- len(r.ArgsContentFilters) == 0 &&
- len(r.ArgsNameFilters) == 0 &&
- !r.BodyDrop && !r.HeadersDrop && !r.ArgsDrop {
- log.Warningf("no filters, returning original request")
- return r.req
- }
- r2 := ParsedRequest{}
- r.FilterHeaders(&r2)
- r.FilterBody(&r2)
- r.FilterArgs(&r2)
- return &r2
- }
- func (r *ReqDumpFilter) ToJSON() error {
- fd, err := os.CreateTemp("", "crowdsec_req_dump_*.json")
- if err != nil {
- return fmt.Errorf("while creating temp file: %w", err)
- }
- defer fd.Close()
- enc := json.NewEncoder(fd)
- enc.SetIndent("", " ")
- req := r.GetFilteredRequest()
- log.Tracef("dumping : %+v", req)
- if err := enc.Encode(req); err != nil {
- //Don't clobber the temp directory with empty files
- err2 := os.Remove(fd.Name())
- if err2 != nil {
- log.Errorf("while removing temp file %s: %s", fd.Name(), err)
- }
- return fmt.Errorf("while encoding request: %w", err)
- }
- log.Infof("request dumped to %s", fd.Name())
- return nil
- }
- // Generate a ParsedRequest from a http.Request. ParsedRequest can be consumed by the App security Engine
- func NewParsedRequestFromRequest(r *http.Request, logger *logrus.Entry) (ParsedRequest, error) {
- var err error
- contentLength := r.ContentLength
- if contentLength < 0 {
- contentLength = 0
- }
- body := make([]byte, contentLength)
- if r.Body != nil {
- _, err = io.ReadFull(r.Body, body)
- if err != nil {
- return ParsedRequest{}, fmt.Errorf("unable to read body: %s", err)
- }
- // reset the original body back as it's been read, i'm not sure its needed?
- r.Body = io.NopCloser(bytes.NewBuffer(body))
- }
- clientIP := r.Header.Get(IPHeaderName)
- if clientIP == "" {
- return ParsedRequest{}, fmt.Errorf("missing '%s' header", IPHeaderName)
- }
- clientURI := r.Header.Get(URIHeaderName)
- if clientURI == "" {
- return ParsedRequest{}, fmt.Errorf("missing '%s' header", URIHeaderName)
- }
- clientMethod := r.Header.Get(VerbHeaderName)
- if clientMethod == "" {
- return ParsedRequest{}, fmt.Errorf("missing '%s' header", VerbHeaderName)
- }
- clientHost := r.Header.Get(HostHeaderName)
- if clientHost == "" { //this might be empty
- logger.Debugf("missing '%s' header", HostHeaderName)
- }
- // delete those headers before coraza process the request
- delete(r.Header, IPHeaderName)
- delete(r.Header, HostHeaderName)
- delete(r.Header, URIHeaderName)
- delete(r.Header, VerbHeaderName)
- originalHTTPRequest := r.Clone(r.Context())
- originalHTTPRequest.Body = io.NopCloser(bytes.NewBuffer(body))
- originalHTTPRequest.RemoteAddr = clientIP
- originalHTTPRequest.RequestURI = clientURI
- originalHTTPRequest.Method = clientMethod
- originalHTTPRequest.Host = clientHost
- parsedURL, err := url.Parse(clientURI)
- if err != nil {
- return ParsedRequest{}, fmt.Errorf("unable to parse url '%s': %s", clientURI, err)
- }
- var remoteAddrNormalized string
- host, _, err := net.SplitHostPort(r.RemoteAddr)
- if err != nil {
- log.Errorf("Invalid appsec remote IP source %v: %s", r.RemoteAddr, err.Error())
- remoteAddrNormalized = r.RemoteAddr
- } else {
- ip := net.ParseIP(host)
- if ip == nil {
- log.Errorf("Invalid appsec remote IP address source %v", r.RemoteAddr)
- remoteAddrNormalized = r.RemoteAddr
- } else {
- remoteAddrNormalized = ip.String()
- }
- }
- return ParsedRequest{
- RemoteAddr: r.RemoteAddr,
- UUID: uuid.New().String(),
- ClientHost: clientHost,
- ClientIP: clientIP,
- URI: parsedURL.Path,
- Method: clientMethod,
- Host: r.Host,
- Headers: r.Header,
- URL: r.URL,
- Proto: r.Proto,
- Body: body,
- Args: parsedURL.Query(), //TODO: Check if there's not potential bypass as it excludes malformed args
- TransferEncoding: r.TransferEncoding,
- ResponseChannel: make(chan AppsecTempResponse),
- RemoteAddrNormalized: remoteAddrNormalized,
- HTTPRequest: originalHTTPRequest,
- }, nil
- }
|