1

Sorry, kind of a long question. I am hoping that someone smarter about InfoSec than I am will read what I'm doing and give me guidance...

Background

My website allows psychiatrists to track their patient's mental health status. It does this by sending a periodic email to a patient, with a link back to a page on the website that asks the patient to rate various aspects of their mental health (for an example of the types of questions asked, Google "PHQ-9").

The link in the email includes two URL arguments:

  1. The primary key of a record in the database that identifies both the patient and the doctor
  2. A random number that must match data that was generated and stored in the Patient record in the database when the email was sent.

Note that the database also contains an expiration date for the most recent email, so the "random number" is only valid for a limited period of time.

Anyone who gets their hands on one of these emails can easily impersonate a patient on our platform by visiting the URL with valid arguments.

Now, a few things are important to note:

  1. The user does NOT need to provide credentials in order to access the page. Anyone who navigates to the page, with valid URL arguments, is presented with the questionnaire. My assumption is that, if someone impersonates a user, the doctor will discover that soon enough when the patient visits the doctor and the questionnaire answers are bogus. Ultimately, the doctor-patient relationship depends on verbal, face-to-face communication. The questionnaire is just additional data the doctor can choose to use or ignore.

  2. Accessing the page does NOT leak patient data. It's a one-way trip from the (presumed) patient's fingers into the website database. The questionnaire does not display any personally identifiable information.

In case someone asks WHY I don't ask for patient credentials, the answer is simple: I want the experience to be as frictionless as possible. I don't want patients to be required to "register" with the website and remember login credentials. The website periodically sends an email to a patient with a link to take an evaluation. The patient clicks the link, and the evaluation appears in a browser window. Done deal. If I require a login exercise then a significant number of patients will simply give up, or forget their password, or whatever.

My question

Is there any obvious security-related flaw with this scheme (other than the risks I've already talked about)? I assume that there are several attack vectors:

  1. Someone might break into a patient's email account. In this case, that person can impersonate the patient, and I have no defense against this. But the worst that can happen is that the bad actor might supply bogus answers to the mental health questionnaires.

  2. Someone might intercept my email on its way to the patient's inbox (e.g. a man-in-the-middle). In this case, the "expiration" feature limits the time frame that the bad actor has to impersonate the patient. This at least prevents someone from using that data after the expiration period has elapsed.

  3. Someone might attempt a brute-force attack by supplying a known valid primary key (discovered from an old email or simply guessed correctly - any reasonably low number is likely to be valid), plus a value for the 32-bit random number. I'm not too worried about this attack.

1

You've hit the main threats, but there's a few that you've missed.

  1. You're leaking information about how many patients you have. It sounds like you're using sequential values for your Patients table's primary key, so exposing that key will tell a patient how many patients you've had before them, and two patients who first visited at different times will be able to determine about how often you add patients. While perhaps not something you care deeply about, there's no need to leak this info.
  2. You mention that the links expire, which is good, but it sounds like you're not automatically expiring them upon use. I recommend that, as soon as the patient submits their information, the link can no longer be used. This is especially important if it would otherwise be possible to view a patient's past answers using the same link.
  3. You don't mention how the random numbers - which are essentially authentication tokens - are generated. If it's using the standard random number generators common to all major programming languages, then a malicious user can probably guess them correctly. The normal RNG functions are typically seeded from the current time (which narrows the space of values to search quite dramatically, even if the attacker doesn't know to the second when the value was created), and also can usually be predicted based on other numbers from the sequence (so one or more patients, whose own numbers are known, could predict the numbers of other patients forward and backward of themself/ves).
  4. 32 bits - even if they are generated using a secure random number generator and thus unpredictable - is shorter than ideal. Realistically, nobody is going to try the ~2 billion requests it would take to guess one by brute-force, given the low sensitivity of the site, but it's not actually impossible to send requests at a rate fast enough to make that feasible (your server is the only bottleneck; an attacker with even the short use a botnet can generate billions of requests, though usually this just knocks the server offline).

Now, none of these are high-severity risks, especially since there really isn't that much at stake. Still, you asked for advice, so... Given all of that, here are some recommendations:

  • Remove the plaintext primary key from the URLs. Either just use the "random number" (token) to identify patients, or pass the primary key through a HMAC (with a key that is not exposed to the patients) so they cannot tell what their number is (or make a meaningful guess at anybody else's valid number). You can create a database index on the token to ensure you're still able to quickly look up patients using it.
  • Lengthen the token to at least 64 bits - 128 is the usual - and use a cryptographically-secure [pseudo-]random number generator to create it. A type 4 GUID/UUID works, if you have a handy tool to emit them (note that not all types of UUIDs are suitable here; it's not actually what the data type is for).
  • After the patient submits their survey answers, invalidate their token (you could remove it entirely, or just use the expiry field to mark it as no longer valid). Don't invalidate URLs that are visited but the survey isn't submitted (until they expire); some email programs proactively send requests to URLs found in the email, to check them for malicious responses. Do continue to expire the tokens the way you currently do, for surveys that are never taken.

Additionally, just to be clear, you need to generate a new token for each email, rather than just reusing them for repeat patients. It sounds like you're already doing this but I wanted to be explicit about it.

Overall, good stab at a threat model and some mitigations.

  • I agree with all of the above. The only small change I would suggest is that I really doubt there is any reason to have the primary key in there in any form, either as the I'd or as an HMAC. Therefore, just kill it. Make sure the token is unique and then you can look up the primary key from the token. You'll want the token column indexed for quick lookups, so adding a unique constraint on the token column will kill two birds with one stone – Conor Mancone Aug 31 at 16:27
  • Thanks! Now that I've read the answer(s), I realize that the bad smell I had was mostly about exposing the primary keys. I'm actually doing the "invalidate upon use" thing already, and I was planning on cryptographically obscuring the primary key, but I like the idea of nuking both the key and the token and using a token (a Type 4 GUID) that exists in a table that gets a new entry each time I send a new email. – Bob.at.Indigo.Health Aug 31 at 20:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.