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 }