Home · About · Services · Industries · Projects · Products · Process · Blog · FAQ · Contact
Protecting patient data in healthcare web apps: a practical guide The engineering controls that keep patient records safe in a clinic or hospital web app: access checks per record, encryption, clean logs, audit trails and…
A receptionist changes the number at the end of a URL, from /patients/1041 to /patients/1042, and sees someone else's lab results. No firewall was bypassed and no password was cracked. The application simply never checked whether that user was allowed to see that record. Most patient data exposures in web apps look like this: ordinary bugs in ordinary places, not exotic attacks.
Short answer: protect patient data in a healthcare web app by checking authorisation on every record, not just every page; encrypting data in transit and at rest; keeping patient details out of logs, URLs and third-party tools; recording who viewed or changed each record; and never using real patient data in development or test environments.
This guide is for engineers building these systems and the people commissioning them. It covers practices that help with obligations under laws such as HIPAA in the US, GDPR in Europe and India's Digital Personal Data Protection Act. Software alone does not make an organisation compliant with any of them. Policies, contracts and training matter just as much, and a qualified adviser should confirm what applies to you.
Map where patient data actually goes Before choosing controls, list every place a patient's details can end up. The main database is the obvious one. The others are where leaks usually happen, because nobody thought of them as storing health data.
Application and web server logs, including request URLs and error traces. Error tracking and analytics tools, which may capture form fields, page titles or request bodies. Backups and database dumps, wherever they are copied to. Email and SMS reminders, which pass through external providers and land on shared phones. Exported spreadsheets and PDFs downloaded to staff laptops. Prompts sent to AI services, if the app drafts summaries or answers questions. The browser itself: cached API responses, local storage and autofill on a shared front-desk computer. Write this map down and keep it with the project documentation. Each item gets a control or a conscious decision to accept the risk. A data map also makes it much easier to answer a patient's request to see or delete their information.
Check access on every record, not every page Role-based access decides which screens someone can open. It does not stop the URL-changing problem above. That needs object-level authorisation: every query that returns patient data is filtered by what the current user is allowed to see. The safest pattern is to make the filtered query the default, so a developer has to go out of their way to bypass it.
# Django REST Framework: scope every query to the user's clinic and role
class VisitViewSet(viewsets.ModelViewSet):
serializer_class = VisitSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
qs = Visit.objects.filter(clinic=user.clinic)
if user.role == "doctor":
qs = qs.filter(doctor=user)
return qs
# A request for a visit outside this queryset returns 404, not 403,
# so the response does not confirm that the record exists.For a second layer, PostgreSQL row-level security can enforce the same rule inside the database. The application sets the current user at the start of each transaction, and a policy filters rows even if a query in the code forgets to. Note that table owners bypass policies unless you add FORCE, so the application should connect as a separate role that does not own the tables.
ALTER TABLE visits ENABLE ROW LEVEL SECURITY;
ALTER TABLE visits FORCE ROW LEVEL SECURITY;
CREATE POLICY visits_by_clinic ON visits
USING (clinic_id = current_setting('app.clinic_id')::bigint);
-- In the application, inside each transaction:
-- SET LOCAL app.clinic_id = '12';Test this explicitly. For every endpoint that returns patient data, write an automated test that logs in as a user from another clinic, or a doctor without that patient, and asserts that the record cannot be read or changed. These tests are cheap and catch the most common class of health data bug.
Encrypt in transit, at rest, and for the most sensitive fields Use TLS for every connection, including the ones inside your network: application to database, application to cache, and services talking to each other. Azure's managed databases encrypt storage at rest by default. On Amazon RDS, confirm encryption is on when you create the instance, because an existing unencrypted instance can only be fixed by restoring from an encrypted snapshot copy. On your own servers, use full-disk encryption. That protects against a lost disk or a discarded snapshot, but not against someone with application or database access.
For the most sensitive fields, such as national ID numbers, diagnoses in free text or mental health notes, consider field-level encryption in the application with keys held in a key management service like AWS KMS or Azure Key Vault. Then a database dump on its own is not readable. The trade-off is that you cannot search or sort encrypted fields directly. If staff need to look patients up by an ID number, store a keyed hash (an HMAC) of it alongside the encrypted value and search on that.
At the web server, send headers that stop browsers from keeping copies of patient data and from leaking URLs to other sites.
server {
# ... listen 443 ssl, certificates, etc.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
location /api/ {
# add_header in a location replaces the server-level ones,
# so repeat them here
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Cache-Control "no-store" always;
proxy_pass http://app:8000;
}
}Keep patient details out of logs, URLs and third-party tools Logs are copied, shipped, retained for months and read by people who would never be given access to the patient database. Treat them as a place patient data must not go.
Use internal IDs in URLs, never names, phone numbers or ID numbers. Put search terms in POST bodies rather than query strings. Log the request path without the query string. In NGINX, a custom log_format using $request_method $uri instead of $request does this. Configure error trackers not to send request bodies, cookies or user details. In the Sentry SDKs, leave send_default_pii off and scrub fields such as name, dob and notes. Do not load third-party analytics or chat widgets on pages that show patient records. They can capture page titles and on-screen text. Keep reminder messages generic: an appointment time and clinic name, not the reason for the visit. If AI features send text to a model provider, use an enterprise agreement with data retention terms you have read, and send only what the task needs. Record who looked at what An audit trail answers the question every clinic eventually asks: who opened this patient's record, and why? Log reads as well as changes, because most misuse in healthcare is people looking at records they had no reason to see. Store entries in a table the application can add to but not edit.
CREATE TABLE audit_log (
id bigserial PRIMARY KEY,
at timestamptz NOT NULL DEFAULT now(),
actor_id bigint NOT NULL,
action text NOT NULL, -- view, create, update, export, print
patient_id bigint,
record_type text,
record_id bigint,
ip inet
);
REVOKE ALL ON audit_log FROM app_user;
GRANT INSERT, SELECT ON audit_log TO app_user;
GRANT USAGE ON SEQUENCE audit_log_id_seq TO app_user;Exports and prints deserve their own entries, since that is where data leaves the system. Give practice managers a simple screen to search the log by patient or by staff member. If only an engineer can read it, nobody will. Copy audit entries to backup storage the application server cannot delete from, so the trail survives a compromised server.
Accounts, sessions and environments Clinic computers are shared and often left logged in. Design for that rather than hoping it will not happen.
Require multi-factor authentication for all staff, and for every administrator account without exception. Give each person their own login. Shared front-desk accounts make the audit trail useless. Use short idle timeouts on clinical screens, with a quick way to sign back in, so staff do not disable them in frustration. Hash passwords with a modern algorithm. In Django, install argon2-cffi and put Argon2PasswordHasher first in PASSWORD_HASHERS. Remove access the day someone leaves, and review active accounts every quarter. Never copy production data into development or staging. Use generated test data, or an anonymised copy produced by a reviewed script that replaces names, contact details and free-text notes. Backups need the same care as the live database: encrypted before they leave the server, stored where a compromised server cannot delete them, and restored on a schedule to prove they work. A backup nobody has restored is a hope, not a plan.
A checklist before launch Run through this before a healthcare app goes live, and again after any major change.
A written data map, covering logs, backups, email, SMS, exports and AI services. Object-level authorisation tests for every endpoint that returns patient data. TLS everywhere, storage encryption on, and field-level encryption for the most sensitive fields. No-store caching and strict referrer headers on patient data responses. No patient details in URLs, logs, error reports or analytics. An append-only audit log of views, changes, exports and prints, readable by practice managers. MFA for staff, individual accounts, sensible timeouts and a leaver process. No real patient data outside production, and tested, encrypted, off-server backups. If several of these are missing from a system already in use, start with authorisation tests and logs. They cover the most likely exposures and need no change to how staff work. Then bring in an independent review of the code and infrastructure, ideally by someone who did not build it.
Infigini IT Services · 11 Kanwali Road, Dehradun 248001, India · info@infigini.com · +91 95684 87789