mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-07 10:26:01 -10:00
Improved handling of nested schema references
This commit is contained in:
1 parent
6a01afb367
commit
c85512dfe0
3 files changed
+213
-170
No files matched your search
+1
-1
@@ -46,7 +46,7 @@ $ sj brute -u https://petstore.swagger.io`,
|
||||
log.Error("Command not specified. See the --help flag for usage.")
|
||||
}
|
||||
},
|
||||
Version: "1.6.5",
|
||||
Version: "1.6.6",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SchemaReference struct {
|
||||
Required []string
|
||||
Type string
|
||||
Properties struct {
|
||||
Property struct {
|
||||
Type string
|
||||
Format string
|
||||
Example string
|
||||
Reference string
|
||||
XML struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Prefix string
|
||||
Attribute bool
|
||||
Wrapped bool
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleSchemaReference(schema *openapi3.SchemaRef) {
|
||||
|
||||
}
|
||||
|
||||
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef, counter int) SwaggerRequest {
|
||||
if param != nil {
|
||||
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
|
||||
if s.Def.Components.Schemas[name] != nil {
|
||||
schema := s.Def.Components.Schemas[name]
|
||||
if schema.Value.Properties != nil {
|
||||
for property := range schema.Value.Properties {
|
||||
if schema.Value.Properties[property].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
|
||||
break
|
||||
}
|
||||
} else if location == "path" {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
|
||||
}
|
||||
}
|
||||
} else if location == "query" {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Query.Add(param.Value.Name, "test")
|
||||
} else {
|
||||
s.Query.Add(param.Value.Name, "1")
|
||||
}
|
||||
} else if location == "body" {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Body[param.Value.Name] = "test"
|
||||
} else {
|
||||
s.Body[param.Value.Name] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.Enum != nil {
|
||||
if location == "path" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", fmt.Sprint(value))
|
||||
} else if location == "query" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Query.Add(param.Value.Name, fmt.Sprint(value))
|
||||
} else if location == "body" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Body[param.Value.Name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
|
||||
if s.Def.Components.Schemas[name] != nil {
|
||||
schema := s.Def.Components.Schemas[name]
|
||||
if schema.Value.Properties != nil {
|
||||
for property := range schema.Value.Properties {
|
||||
if schema.Value.Properties[property].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Body[property] = "test"
|
||||
} else {
|
||||
s.Body[property] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.Enum != nil {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Body[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
+93
-169
@@ -6,7 +6,6 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -232,40 +231,7 @@ func (s SwaggerRequest) BuildDefinedRequests(client http.Client, method string,
|
||||
}
|
||||
}
|
||||
} else if os.Args[1] == "prepare" {
|
||||
if strings.ToLower(prepareFor) == "curl" {
|
||||
if s.BodyData == nil {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("curl -sk -X %s '%s'\n", method, s.URL.String())
|
||||
} else {
|
||||
fmt.Printf("curl -sk -X %s '%s' -H '%s'\n", method, s.URL.String(), strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
} else {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("curl -sk -X %s '%s' -d '%s'\n", method, s.URL.String(), s.BodyData)
|
||||
} else {
|
||||
fmt.Printf("curl -sk -X %s '%s' -d '%s' -H '%s'\n", method, s.URL.String(), s.BodyData, strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
}
|
||||
} else if strings.ToLower(prepareFor) == "sqlmap" {
|
||||
if s.BodyData == nil {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("sqlmap -u %s\n", s.URL.String())
|
||||
} else {
|
||||
fmt.Printf("sqlmap -u %s -H '%s'\n", s.URL.String(), strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
} else {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("sqlmap -u %s --data='%s'\n", s.URL.String(), s.BodyData)
|
||||
} else {
|
||||
fmt.Printf("sqlmap -u %s --data='%s' -H '%s'\n", s.URL.String(), s.BodyData, strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
}
|
||||
} else if strings.ToLower(prepareFor) == "ffuf" {
|
||||
// TODO
|
||||
} else {
|
||||
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
|
||||
}
|
||||
|
||||
s.PrintPreparedCommands(method)
|
||||
} else if os.Args[1] == "endpoints" {
|
||||
|
||||
for k := range s.Def.Paths {
|
||||
@@ -286,7 +252,7 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
|
||||
} else if param.Value.In == "path" {
|
||||
if param.Value.Schema != nil {
|
||||
if param.Value.Schema.Ref != "" {
|
||||
s.SetParametersFromSchema(param, "path", param.Value.Schema.Ref, nil)
|
||||
s = s.SetParametersFromSchema(param, "path", param.Value.Schema.Ref, nil, 0)
|
||||
} else if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
|
||||
@@ -310,7 +276,7 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
|
||||
} else if param.Value.In == "query" {
|
||||
if param.Value.Schema != nil {
|
||||
if param.Value.Schema.Ref != "" {
|
||||
s.SetParametersFromSchema(param, "query", param.Value.Schema.Ref, nil)
|
||||
s = s.SetParametersFromSchema(param, "query", param.Value.Schema.Ref, nil, 0)
|
||||
} else {
|
||||
if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
|
||||
s.Query.Add(param.Value.Name, "test")
|
||||
@@ -326,7 +292,7 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
|
||||
Headers = append(Headers, fmt.Sprintf("%s: %s", param.Value.Name, "1"))
|
||||
} else if param.Value.In == "body" {
|
||||
if param.Value.Schema.Ref != "" {
|
||||
s.SetParametersFromSchema(param, "body", param.Value.Schema.Ref, nil)
|
||||
s = s.SetParametersFromSchema(param, "body", param.Value.Schema.Ref, nil, 0)
|
||||
}
|
||||
if param.Value.Schema.Value.Type == "string" {
|
||||
s.Body[param.Value.Name] = "test"
|
||||
@@ -346,79 +312,65 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
|
||||
if op.RequestBody != nil {
|
||||
if op.RequestBody.Value.Content != nil {
|
||||
for i := range op.RequestBody.Value.Content {
|
||||
if contentType == "" && Headers != nil && strings.Contains(strings.ToLower(strings.Join(Headers, ",")), "content-type") {
|
||||
headerString := strings.Join(Headers, ",")
|
||||
Headers = nil
|
||||
ctIndex := strings.Index(strings.ToLower(headerString), "content-type:") + 14
|
||||
headerString = headerString[ctIndex:]
|
||||
if strings.Contains(headerString, ",") {
|
||||
headerString = strings.TrimPrefix(headerString, ",")
|
||||
ctEndIndex := strings.Index(headerString[ctIndex:], ",") + 1
|
||||
headerString = headerString[:ctEndIndex]
|
||||
} else if !strings.Contains(strings.ToLower(headerString), "content-type") && (strings.Contains(strings.ToLower(headerString), "application/x-www-form-urlencoded") || strings.Contains(strings.ToLower(headerString), "application/json")) {
|
||||
headerString = ""
|
||||
}
|
||||
if headerString != "" {
|
||||
Headers = append(Headers, strings.Split(headerString, ",")...)
|
||||
}
|
||||
|
||||
if contentType == "" {
|
||||
EnforceSingleContentType(i)
|
||||
} else {
|
||||
EnforceSingleContentType(contentType)
|
||||
}
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value == nil {
|
||||
s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Ref, op.RequestBody)
|
||||
s = s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Ref, op.RequestBody, 0)
|
||||
if strings.Contains(i, "json") {
|
||||
s.BodyData, _ = json.Marshal(s.Body)
|
||||
}
|
||||
} else if i == "application/x-www-form-urlencoded" {
|
||||
if contentType == "" && !strings.Contains(strings.ToLower(strings.Join(Headers, "")), "content-type") {
|
||||
Headers = append(Headers, "Content-Type: application/x-www-form-urlencoded")
|
||||
}
|
||||
var formData []string
|
||||
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
|
||||
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
|
||||
if valueType == "string" {
|
||||
s.Body[j] = "test"
|
||||
} else if valueType == "boolean" {
|
||||
s.Body[j] = false
|
||||
} else if valueType == "integer" || valueType == "number" {
|
||||
s.Body[j] = 1
|
||||
} else {
|
||||
s.Body[j] = "unknown_type_populate_manually"
|
||||
}
|
||||
} else if strings.Contains(i, "x-www-form-urlencoded") {
|
||||
var formData []string
|
||||
for j := range s.Body {
|
||||
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
|
||||
}
|
||||
s.BodyData = []byte(strings.Join(formData, "&"))
|
||||
} else if strings.Contains(i, "xml") {
|
||||
// TODO
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
s.BodyData = []byte(strings.Join(formData, "&"))
|
||||
} else if strings.Contains(i, "json") {
|
||||
if contentType == "" && !strings.Contains(strings.ToLower(strings.Join(Headers, "")), "content-type") {
|
||||
Headers = append(Headers, "Content-Type: application/json")
|
||||
}
|
||||
} else {
|
||||
var formData []string
|
||||
|
||||
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Ref != "" {
|
||||
s = s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Ref, op.RequestBody, 0)
|
||||
} else {
|
||||
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
|
||||
if valueType == "string" {
|
||||
s.Body[j] = "test"
|
||||
} else if valueType == "boolean" {
|
||||
s.Body[j] = false
|
||||
} else if valueType == "integer" || valueType == "number" {
|
||||
s.Body[j] = 1
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
|
||||
if valueType == "string" {
|
||||
s.Body[j] = "test"
|
||||
} else if valueType == "boolean" {
|
||||
s.Body[j] = false
|
||||
} else if valueType == "integer" || valueType == "number" {
|
||||
s.Body[j] = 1
|
||||
} else {
|
||||
s.Body[j] = "unknown_type_populate_manually"
|
||||
}
|
||||
if i == "application/x-www-form-urlencoded" {
|
||||
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
|
||||
}
|
||||
}
|
||||
|
||||
if i == "application/x-www-form-urlencoded" {
|
||||
s.BodyData = []byte(strings.Join(formData, "&"))
|
||||
} else if strings.Contains(i, "json") || i == "*/*" {
|
||||
s.BodyData, _ = json.Marshal(s.Body)
|
||||
} else if strings.Contains(i, "xml") {
|
||||
for element := range s.Body {
|
||||
fmt.Printf("<%s>test</%s>\n", element, element)
|
||||
}
|
||||
|
||||
//s.BodyData, _ = xml.Marshal(s.Body)
|
||||
} else {
|
||||
s.Body[j] = "unknown_type_populate_manually"
|
||||
s.Body["test"] = "test"
|
||||
s.BodyData = []byte("test=test")
|
||||
}
|
||||
}
|
||||
}
|
||||
s.BodyData, _ = json.Marshal(s.Body)
|
||||
} else if strings.Contains(i, "multipart/form-data") {
|
||||
//TODO
|
||||
s.Body["MULTIPART_FORM_DATA"] = "TEST_MANUALLY"
|
||||
s.BodyData = []byte("MULTIPART_FORM_DATA=TEST_MANUALLY")
|
||||
} else if strings.Contains(i, "xml") {
|
||||
//TODO
|
||||
s.Body["XML"] = "TEST_MANUALLY"
|
||||
s.BodyData = []byte("XML=TEST_MANUALLY")
|
||||
} else {
|
||||
s.Body["test"] = "test"
|
||||
s.BodyData = []byte("test=test")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -430,80 +382,31 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
|
||||
return s
|
||||
}
|
||||
|
||||
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef) {
|
||||
if param != nil {
|
||||
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
|
||||
if s.Def.Components.Schemas[name] != nil {
|
||||
schema := s.Def.Components.Schemas[name]
|
||||
if schema.Value.Properties != nil {
|
||||
for property := range schema.Value.Properties {
|
||||
if schema.Value.Properties[property].Ref != "" {
|
||||
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
|
||||
break
|
||||
} else if schema.Value.Properties[property].Value.Type == "string" {
|
||||
if location == "path" {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
|
||||
}
|
||||
}
|
||||
} else if location == "query" {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Query.Add(param.Value.Name, "test")
|
||||
} else {
|
||||
s.Query.Add(param.Value.Name, "1")
|
||||
}
|
||||
} else if location == "body" {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Body[param.Value.Name] = "test"
|
||||
} else {
|
||||
s.Body[param.Value.Name] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.Enum != nil {
|
||||
if location == "path" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", fmt.Sprint(value))
|
||||
} else if location == "query" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Query.Add(param.Value.Name, fmt.Sprint(value))
|
||||
} else if location == "body" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Body[param.Value.Name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s SwaggerRequest) PrintPreparedCommands(method string) {
|
||||
var preparedCommand string
|
||||
if strings.ToLower(prepareFor) == "curl" {
|
||||
preparedCommand = fmt.Sprintf("curl -sk -X %s '%s'", method, s.URL.String())
|
||||
} else if strings.ToLower(prepareFor) == "sqlmap" {
|
||||
preparedCommand = fmt.Sprintf("sqlmap --method=%s -u %s", method, s.URL.String())
|
||||
} else if strings.ToLower(prepareFor) == "ffuf" {
|
||||
log.Fatal("ffuf is not supported yet :(")
|
||||
// TODO
|
||||
} else {
|
||||
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
|
||||
if s.Def.Components.Schemas[name] != nil {
|
||||
schema := s.Def.Components.Schemas[name]
|
||||
if schema.Value.Properties != nil {
|
||||
for property := range schema.Value.Properties {
|
||||
if schema.Value.Properties[property].Ref != "" {
|
||||
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
|
||||
break
|
||||
} else {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Body[property] = "test"
|
||||
} else {
|
||||
s.Body[property] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.Enum != nil {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Body[schema.Value.Title] = value
|
||||
}
|
||||
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
|
||||
}
|
||||
|
||||
if s.BodyData != nil {
|
||||
if strings.ToLower(prepareFor) == "sqlmap" {
|
||||
preparedCommand = preparedCommand + fmt.Sprintf(" --data='%s'", s.BodyData)
|
||||
} else {
|
||||
preparedCommand = preparedCommand + fmt.Sprintf(" -d '%s'", s.BodyData)
|
||||
}
|
||||
}
|
||||
if len(Headers) != 0 {
|
||||
preparedCommand = preparedCommand + fmt.Sprintf(" -H '%s'", strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
|
||||
fmt.Println(preparedCommand)
|
||||
}
|
||||
|
||||
func (s SwaggerRequest) GetBasePath() string {
|
||||
@@ -742,3 +645,24 @@ func CheckAndConfigureProxy() (client http.Client) {
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func EnforceSingleContentType(newContentType string) {
|
||||
if Headers != nil {
|
||||
headerString := strings.Join(Headers, ",")
|
||||
Headers = nil
|
||||
ctIndex := strings.Index(strings.ToLower(headerString), "content-type:") + 14
|
||||
headerString = headerString[ctIndex:]
|
||||
if strings.Contains(headerString, ",") {
|
||||
headerString = strings.TrimPrefix(headerString, ",")
|
||||
ctEndIndex := strings.Index(headerString[ctIndex:], ",") + 1
|
||||
headerString = headerString[:ctEndIndex]
|
||||
} else if !strings.Contains(headerString, ":") {
|
||||
headerString = ""
|
||||
}
|
||||
if headerString != "" {
|
||||
Headers = append(Headers, strings.Split(headerString, ",")...)
|
||||
}
|
||||
}
|
||||
|
||||
Headers = append(Headers, "Content-Type: "+newContentType)
|
||||
}
|
||||
Reference in new issue
Block a user