Skip to content

Fix: prevent 500 error when request body is not JSON object#3562

Open
RCcoders wants to merge 2 commits intoohcnetwork:developfrom
RCcoders:fix-request-body-validation
Open

Fix: prevent 500 error when request body is not JSON object#3562
RCcoders wants to merge 2 commits intoohcnetwork:developfrom
RCcoders:fix-request-body-validation

Conversation

@RCcoders
Copy link

@RCcoders RCcoders commented Mar 6, 2026

Summary

Fixes a Broken Object Level Authorization (BOLA) vulnerability in
PatientViewSet.update_identifier.

Previously, a user could update a patient identifier using a
PatientIdentifierConfig belonging to another facility if they knew
the configuration UUID.

Fix

Added facility-level validation before updating identifiers:

  • Ensure the identifier configuration belongs to the correct facility context.
  • Enforce authorization using
    can_write_facility_patient_identifier_config.

Tests

Added a security test (test_patient_security_api.py) that verifies:

  • Cross-facility identifier updates return 403 Forbidden
  • No identifier is created in the database

All existing tests pass locally.

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced input validation across API endpoints to properly handle and reject malformed requests.
    • Added authorization checks for facility-specific configuration access and updates.
    • Improved robustness of facility data handling with safer fallback values.
  • Tests

    • Added security validation tests for configuration access control scenarios.

@RCcoders RCcoders requested a review from a team as a code owner March 6, 2026 18:28
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

This pull request adds input body type validation to facility and patient API viewsets, introduces facility-context authorization checks for identifier updates, replaces unsafe dictionary indexing with safe .get() lookups in facility specs, and includes a new security test for BOLA vulnerability prevention.

Changes

Cohort / File(s) Summary
API Input Validation
care/emr/api/viewsets/facility.py, care/emr/api/viewsets/patient.py
Added ValidationError checks to ensure request.data is a dict in multiple POST actions (set_monetary_config, set_invoice_expression, search, add_user, delete_user, update_identifier). In patient.py, authorization checks validate that identifier configs belong to the current facility context and user has write permissions.
Facility Resource Spec
care/emr/resources/facility/spec.py
Replaced direct index-access lookups on facility type mappings with .get() calls, introducing "Unknown" as a safe default fallback for missing keys instead of raising KeyError exceptions.
Security Test Coverage
care/emr/tests/test_patient_security_api.py
Added new test module with TestPatientSecurityAPI class that verifies HTTP 403 Forbidden response and prevents identifier creation when using a configuration from a different facility (BOLA vulnerability scenario).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change—adding request body validation to prevent 500 errors—which is reflected in the actual code changes across multiple viewsets.
Description check ✅ Passed The description covers the key changes (BOLA vulnerability fix, facility-level validation, security test) though it emphasizes the security aspect more than the request body validation mentioned in the title.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@care/emr/api/viewsets/patient.py`:
- Line 414: The code in update_identifier uses facility =
self.get_serializer_list_context().get("facility"), which incorrectly enforces
can_list_facility_tag_config and couples identifier updates to tag-list
permissions; instead, fetch the facility directly without calling
get_serializer_list_context to avoid that auth gate. Replace that lookup with a
direct facility resolver (e.g., use an existing helper like self.get_facility(),
derive from self.request/kwargs, or query Facility by the provided facility_id)
inside update_identifier so it no longer depends on
get_serializer_list_context() or can_list_facility_tag_config.
- Around line 416-420: The cross-facility guard currently only runs when
facility is provided, allowing callers to omit facility and bypass the check;
make the facility context mandatory or derive it server-side and enforce the
check unconditionally: ensure the code handling request_config.facility always
has a resolved facility (e.g., derive from the patient record or current
request/user context) and if no facility can be resolved raise PermissionDenied,
then compare request_config.facility.id with the resolved facility.id (same
symbols: request_config.facility, facility, PermissionDenied) so the mismatch
cannot be skipped by omitting facility.

In `@care/emr/resources/facility/spec.py`:
- Line 175: Do not invent a new serialized value "Unknown"; instead stop using
the "Unknown" default and preserve the canonical mapping or null/absence when a
mapping is missing. Replace mapping["facility_type"] =
REVERSE_FACILITY_TYPES.get(obj.facility_type, "Unknown") with either
mapping["facility_type"] = REVERSE_FACILITY_TYPES.get(obj.facility_type) (so it
becomes None/null when missing) or only set mapping["facility_type"] if
obj.facility_type in REVERSE_FACILITY_TYPES; apply the same change for the other
occurrence at the noted line.
- Line 157: The assignment silently maps unknown facility_type to None; change
it to fail fast by checking REVERSE_REVERSE_FACILITY_TYPES for
self.facility_type before assignment: if a mapping exists assign
obj.facility_type, otherwise raise a validation error (e.g., ValueError or the
module's ValidationError) with a clear message referencing the invalid
self.facility_type; use the symbols REVERSE_REVERSE_FACILITY_TYPES,
self.facility_type, and obj.facility_type to locate and update the logic in the
relevant method.

In `@care/emr/tests/test_patient_security_api.py`:
- Around line 19-27: The test fails because the user lacks the facility-scoped
identifier-config write permission and the request payload doesn't include the
facility, so the endpoint rejects the call before the BOLA cross-facility check
runs; update the test to grant the appropriate facility-scoped permission via
create_role_with_permissions (add the identifier-config write permission enum
from your permissions set) and attach it to the user with
attach_role_organization_user for the current facility context, and modify the
test request payload to include the facility field (use the same facility
instance used when attaching the role) so the BOLA guard is actually exercised;
apply the same changes to the related tests referenced in lines 31-53.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 100928ff-d408-4eeb-9c0a-5fc0b33c455b

📥 Commits

Reviewing files that changed from the base of the PR and between d2a2c2f and 6f865f6.

📒 Files selected for processing (4)
  • care/emr/api/viewsets/facility.py
  • care/emr/api/viewsets/patient.py
  • care/emr/resources/facility/spec.py
  • care/emr/tests/test_patient_security_api.py

PatientIdentifierConfig, external_id=request_data.config
)

facility = self.get_serializer_list_context().get("facility")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t resolve identifier context through the tag helper.

get_serializer_list_context() also enforces can_list_facility_tag_config, so update_identifier now depends on tag-view permission just to look up a facility. That’s an unrelated authorization gate and will block legitimate identifier updates in a fairly confusing way.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/api/viewsets/patient.py` at line 414, The code in update_identifier
uses facility = self.get_serializer_list_context().get("facility"), which
incorrectly enforces can_list_facility_tag_config and couples identifier updates
to tag-list permissions; instead, fetch the facility directly without calling
get_serializer_list_context to avoid that auth gate. Replace that lookup with a
direct facility resolver (e.g., use an existing helper like self.get_facility(),
derive from self.request/kwargs, or query Facility by the provided facility_id)
inside update_identifier so it no longer depends on
get_serializer_list_context() or can_list_facility_tag_config.

Comment on lines +416 to +420
# Ensure identifier config belongs to the patient's facility context
if request_config.facility and facility and request_config.facility.id != facility.id:
raise PermissionDenied(
"Identifier configuration does not belong to the patient's facility"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The cross-facility guard is still optional.

This branch only runs when facility is present, so a caller can omit it and skip the mismatch check entirely. If facility-scoped configs must stay inside the current facility context, that context needs to be mandatory here or derived server-side.

🧰 Tools
🪛 Ruff (0.15.4)

[warning] 418-420: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/api/viewsets/patient.py` around lines 416 - 420, The cross-facility
guard currently only runs when facility is provided, allowing callers to omit
facility and bypass the check; make the facility context mandatory or derive it
server-side and enforce the check unconditionally: ensure the code handling
request_config.facility always has a resolved facility (e.g., derive from the
patient record or current request/user context) and if no facility can be
resolved raise PermissionDenied, then compare request_config.facility.id with
the resolved facility.id (same symbols: request_config.facility, facility,
PermissionDenied) so the mismatch cannot be skipped by omitting facility.

external_id=self.geo_organization, org_type="govt"
).first()
obj.facility_type = REVERSE_REVERSE_FACILITY_TYPES[self.facility_type]
obj.facility_type = REVERSE_REVERSE_FACILITY_TYPES.get(self.facility_type)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t silently coerce an invalid facility_type to None.

REVERSE_REVERSE_FACILITY_TYPES.get(self.facility_type) makes unknown input look valid until much later, which is a neat way to trade a clean 400 for harder-to-debug state. Fail fast here instead of storing a missing mapping.

Suggested fix
-        obj.facility_type = REVERSE_REVERSE_FACILITY_TYPES.get(self.facility_type)
+        try:
+            obj.facility_type = REVERSE_REVERSE_FACILITY_TYPES[self.facility_type]
+        except KeyError as exc:
+            raise ValueError("Invalid facility_type") from exc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/resources/facility/spec.py` at line 157, The assignment silently
maps unknown facility_type to None; change it to fail fast by checking
REVERSE_REVERSE_FACILITY_TYPES for self.facility_type before assignment: if a
mapping exists assign obj.facility_type, otherwise raise a validation error
(e.g., ValueError or the module's ValidationError) with a clear message
referencing the invalid self.facility_type; use the symbols
REVERSE_REVERSE_FACILITY_TYPES, self.facility_type, and obj.facility_type to
locate and update the logic in the relevant method.

if obj.created_by:
mapping["created_by"] = model_from_cache(UserSpec, id=obj.created_by_id)
mapping["facility_type"] = REVERSE_FACILITY_TYPES[obj.facility_type]
mapping["facility_type"] = REVERSE_FACILITY_TYPES.get(obj.facility_type, "Unknown")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid inventing "Unknown" as a new serialized facility type.

These endpoints previously returned one of the canonical labels from REVERSE_FACILITY_TYPES. Falling back to "Unknown" hides bad data and can break consumers that compare against the defined facility-type values.

Also applies to: 292-292

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/emr/resources/facility/spec.py` at line 175, Do not invent a new
serialized value "Unknown"; instead stop using the "Unknown" default and
preserve the canonical mapping or null/absence when a mapping is missing.
Replace mapping["facility_type"] = REVERSE_FACILITY_TYPES.get(obj.facility_type,
"Unknown") with either mapping["facility_type"] =
REVERSE_FACILITY_TYPES.get(obj.facility_type) (so it becomes None/null when
missing) or only set mapping["facility_type"] if obj.facility_type in
REVERSE_FACILITY_TYPES; apply the same change for the other occurrence at the
noted line.

@RCcoders
Copy link
Author

RCcoders commented Mar 6, 2026

Thanks for the review! I’ve addressed the CodeRabbit comments in the latest commit.

Changes made:

  • Replaced facility resolution in "update_identifier" to avoid using "get_serializer_list_context()" and removed the unintended permission coupling.
  • Enforced facility context validation to prevent bypassing the cross-facility check.
  • Updated facility type handling in "spec.py" to fail fast on invalid mappings and removed the ""Unknown"" fallback.
  • Fixed the test to grant the correct facility-scoped permission and include the facility in the request payload so the BOLA check is properly exercised.

All tests pass locally after the changes. Please let me know if any further adjustments are needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants