request.go 9.8 KB

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