By Claude:
Issue 1: 403 on context=patient_picture — This Is Intentional in 8.0.0-P3
The document_id=-1 with context=patient_picture URL pattern was identified as an IDOR (Insecure Direct Object Reference) vulnerability — patient photos (PHI) could be retrieved for any patient by any authenticated user with document access, without the server verifying authorization. The expected fix is that the server now checks whether the user may access the patient’s record before serving the photo, and returns 403 if not. GitHub
So the 403 you’re seeing is the security patch working correctly — but it broke legitimate use cases too, as confirmed by the community.
The error log signature is: OpenEMR.WARNING: An attempt was made to retrieve a patient picture for an unauthorized patient {"user-id":"4","requested-patient-id":"2236","session-pid":"0"} — the key is "session-pid":"0", meaning the session doesn’t have the patient loaded when the picture request fires (e.g., on calendar hover). OpenEMR
The root cause: The security fix requires $_SESSION['pid'] to match the requested patient_id. When the request fires outside of an active patient context (like a calendar tooltip, or a custom module calling it directly), session-pid is 0 and it returns 403.
Fix Options
Option A — Set the session pid before the request (proper fix)
Before making the controller.php?...context=patient_picture call, your code (or the custom module) needs to have the patient loaded in session. In OpenEMR, this means the patient must be selected via setpid(). If you’re calling this from a custom module or external page, add:
php
// At the top of your custom page, before rendering the photo URL:
require_once($GLOBALS['srcdir'] . '/pid.inc.php');
setpid($patient_id); // sets $_SESSION['pid']
Option B — Use the authenticated demographics route instead
Rather than calling controller.php directly, load the photo through a page that already has the patient in session context (i.e., navigate to the patient first via interface/patient_file/summary/demographics.php). The photo will load correctly once the patient is active.
Option C — Check the new authorization logic in C_Document.class.php
The new check in 8.0.0-P3 for patient_picture context likely looks something like:
php
if ($_SESSION['pid'] != $patient_id) {
// 403
}
If your use case is legitimately a staff user (not a patient portal session), you can patch C_Document.class.php to also allow access when $_SESSION['authUserID'] is set and the user has document ACL permissions — essentially restoring the pre-patch behavior for authenticated staff while keeping the portal restriction. But be cautious: this is a security regression.
Issue 2: Pop-Out Triggering Download Instead of New Window
Good news: OpenEMR’s C_Document.class.php already controls Content-Disposition based on the as_file parameter — when as_file=false, it serves Content-Disposition: inline; when as_file=true, it serves attachment. GitHub
So the PHP side is already correct for the pop-out case (as_file=false in the URL). The problem is purely on the JavaScript side — the Pop Out button is likely triggering a download action or using the wrong URL. Here’s where to look:
Locate the Pop Out button JS
In OpenEMR 8.x, the document viewer JS is in:
interface/patient_file/documents.php
or rendered via the document list template. Search for popout, pop_out, or window.open in that file.
The fix is to ensure the Pop Out handler uses window.open() and passes as_file=false:
javascript
// Find something like this (broken — may be using location.href or a download link):
document.getElementById('btn_popout').addEventListener('click', function() {
window.location.href = docUrl; // ← this triggers download behavior
});
// Fix — force new window with inline disposition:
document.getElementById('btn_popout').addEventListener('click', function() {
let url = docUrl.replace('as_file=true', 'as_file=false');
window.open(url, '_blank', 'width=900,height=700,noopener,noreferrer');
});
If ChatGPT swapped the Download button to use window.open() when fixing something else, it may have accidentally made both buttons share the same handler or URL. Check that:
-
Download button uses
as_file=true(or justwindow.location.href) -
Pop Out button uses
window.open(url_with_as_file=false, '_blank')