request.go 9.3 KB

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