8.22 Confirming Requests via Email

8.22.1 Problem

You want to allow users to confirm a request via email while preventing third parties from spoofing or falsifying confirmations.

8.22.2 Solution

Generate a random identifier, associate it with the email address to be confirmed, and save it for verification later. Send an email that contains the random identifier, along with instructions for responding to confirm receipt and approval. If a response is received, compare the identifier in the response with the saved identifier for the email address from which the response was received. If the identifiers don't match, ignore the response and do nothing; otherwise, the confirmation was successful.

8.22.3 Discussion

The most common use for confirmation requests is to ensure that an email address actually belongs to the person requesting membership on some kind of mass mailing list (whether it's a mailing list, newsletter, or some other type of mass mailing). Joining a mass mailing list typically involves either sending mail to an automated recipient or filling out a form on a web page.

The problem with this approach is that it is trivial for someone to register someone else's email address with a mailing list. For example, suppose that Alice wants to annoy Bob. If mailing lists accepted email addresses without any kind of confirmation, Alice could register Bob's email address with as many mailing lists as she could find. Suddenly, Bob would begin receiving large amounts of email from mailing lists with which he did not register. In extreme cases, this could lead to denial of service because Bob's mailbox could fill up with unwanted email, or if Bob has a slow network connection, it could take an unreasonable amount of time for him to download his email.

The solution to this problem is to confirm with Bob that he really made the requests for membership with the mailing lists. When a request for membership is sent for a mailing list, the mailing list software can send an email to the address for which membership was requested. This email will ask the recipient to respond with a confirmation that membership is truly desired.

The simplest form of such a confirmation request is to require the recipient to reply with an email containing some nonunique content, such as the word "subscribe" or something similar. This method is easiest for the mailing list software to deal with because it does not have to keep any information about what requests have been made or confirmed. It simply needs to respond to confirmation responses by adding the sender's email address to the mailing list roster.

Unfortunately, this is not an acceptable solution either, because Alice might know what response needs to be sent back to the confirmation request in order for the mailing list software to add Bob to its roster. If Alice knows what needs to be sent, she can easily forge a response email, making it appear to the mailing list software as if it came from Bob's email address.

Sending a confirmation request that requires an affirmative acknowledgement is a step in the right direction, but as we have just described it, it is not enough. Instead of requiring a nonunique acknowledgment, the confirmation request should contain a unique identifier that is generated at the time that the request for membership is made. To confirm the request, the recipient must send back a response that also contains the same unique identifier.

Because a unique identifier is used, it is not possible for Alice to know what she would need to send back to the mailing list software to get Bob's email address on the roster, unless she somehow had access to Bob's email. That would allow her to see the confirmation request and the unique identifier that it contains. Unfortunately, this is a much more difficult problem to solve, and it is one that cannot be easily solved in software, so we will not give it any further consideration.

To implement such a scheme, the mailing list software must maintain some state information. In particular, upon receipt of a request for membership, the software needs to generate the unique identifier to include in the confirmation requests, and it must store that identifier along with the email address for which membership has been requested. In addition, it is a good idea to maintain some kind of a timestamp so that confirmation requests will eventually expire. Expiring confirmation requests significantly reduces the likelihood that Alice can guess the unique identifier; more importantly, it also helps to reduce the amount of information that must be remembered to be able to confirm requests.

We define two functions in this recipe that provide the basic implementation for the confirmation request scheme we have just described. The first, spc_confirmation_create( ), creates a new confirmation request by generating a unique identifier and storing it with the email address for which confirmation is to be requested. It stores the confirmation request information in an in-memory list of pending confirmations, implemented simply as a dynamically allocated array. For use in a production environment, a hash table or binary tree might be a better solution for an in-memory data structure. Alternatively, the information could be stored in a database.

The function spc_confirmation_create( ) (SpcConfirmationCreate() on Windows) will return 0 if some kind of error occurs. Possible errors include memory allocation failures and attempts to add an address to the list of pending confirmations that already exists in the list. If the operation is successful, the return value will be 1. Two arguments are required by spc_confirmation_create( ):

address

Email address that is to be confirmed.

id

Pointer to a buffer that will be allocated by spc_confirmation_create( ). If the function returns successfully, the buffer will contain the unique identifier to send as part of the confirmation request email. It is the responsibility of the caller to free the buffer using free( ) on Unix or LocalFree( ) on Windows.

You may adjust the SPC_CONFIRMATION_EXPIRE macro from the default presented here. It controls how long pending confirmation requests will be honored and is specified in seconds.

Note that the code we are presenting here does not send or receive email at all. Programmatically sending and receiving email is outside the scope of this book.

#include <stdlib.h>
#include <string.h>
#include <time.h>
   
/* Confirmation receipts must be received within one hour (3600 seconds) */
#define SPC_CONFIRMATION_EXPIRE 3600
   
typedef struct {
  char   *address;
  char   *id;
  time_t expire;
} spc_confirmation_t;
   
static unsigned long      confirmation_count, confirmation_size;
static spc_confirmation_t *confirmations;
   
static int new_confirmation(const char *address, const char *id) {
  unsigned long      i;
  spc_confirmation_t *tmp;
   
  /* first make sure that the address isn't already in the list */
  for (i = 0;  i < confirmation_count;  i++)
    if (!strcmp(confirmations[i].address, address)) return 0;
   
  if (confirmation_count =  = confirmation_size) {
    tmp = (spc_confirmation_t *)realloc(confirmations,
          sizeof(spc_confirmation_t) * (confirmation_size + 1));
    if (!tmp) return 0;
    confirmations = tmp;
    confirmation_size++;
  }
  confirmations[confirmation_count].address = strdup(address);
  confirmations[confirmation_count].id = strdup(id);
  confirmations[confirmation_count].expire = time(0) + SPC_CONFIRMATION_EXPIRE;
  if (!confirmations[confirmation_count].address ||
      !confirmations[confirmation_count].id) {
    if (confirmations[confirmation_count].address)
      free(confirmations[confirmation_count].address);
    if (confirmations[confirmation_count].id)
      free(confirmations[confirmation_count].id);
    return 0;
  }
  confirmation_count++;
  return 1;
}
   
int spc_confirmation_create(const char *address, char **id) {
  unsigned char buf[16];
   
  if (!spc_rand(buf, sizeof(buf))) return 0;
  if (!(*id = (char *)spc_base64_encode(buf, sizeof(buf), 0))) return 0;
  if (!new_confirmation(address, *id)) {
    free(*id);
    return 0;
  }
  return 1;
}

Upon receipt of a response to a confirmation request, the address from which it was sent and the unique identified contained within it should be passed as arguments to spc_confirmation_receive( ) (SpcConfirmationReceive() on Windows). If the address and unique identifier are in the list of pending requests, the return from this function will be 1; otherwise, it will be 0. Before the list is checked, expired entries will automatically be removed.

int spc_confirmation_receive(const char *address, const char *id) {
  time_t        now;
  unsigned long i;
   
  /* Before we check the pending list of confirmations, prune the list to
   * remove expired entries.
   */
  now = time(0);
  for (i = 0;  i < confirmation_count;  i++) {
    if (confirmations[i].expire <= now) {
      free(confirmations[i].address);
      free(confirmations[i].id);
      if (confirmation_count > 1 && i < confirmation_count - 1)
        confirmations[i] = confirmations[confirmation_count - 1];
      i--;
      confirmation_count--;
    }
  }
   
  for (i = 0;  i < confirmation_count;  i++) {
    if (!strcmp(confirmations[i].address, address)) {
      if (strcmp(confirmations[i].id, id) != 0) return 0;
      free(confirmations[i].address);
      free(confirmations[i].id);
      if (confirmation_count > 1 && i < confirmation_count - 1)
        confirmations[i] = confirmations[confirmation_count - 1];
      confirmation_count--;
      return 1;
    }
  }
  return 0;
}

The Windows versions of spc_confirmation_create( ) and spc_confirmation_receive( ) are named SpcConfirmationCreate( ) and SpcConfirmationReceive( ), respectively. The arguments and return values for each are the same; however, there are enough subtle differences in the underlying implementation that we present an entirely separate code listing for Windows instead of using the preprocessor to have a single version.

#include <windows.h>
   
/* Confirmation receipts must be received within one hour (3600 seconds) */
#define SPC_CONFIRMATION_EXPIRE 3600
   
typedef struct {
  LPTSTR        lpszAddress;
  LPSTR         lpszID;
  LARGE_INTEGER liExpire;
} SPC_CONFIRMATION;
   
static DWORD            dwConfirmationCount, dwConfirmationSize;
static SPC_CONFIRMATION *pConfirmations;
   
static BOOL NewConfirmation(LPCTSTR lpszAddress, LPCSTR lpszID) {
  DWORD            dwIndex;
  LARGE_INTEGER    liExpire;
  SPC_CONFIRMATION *pTemp;
   
  /* first make sure that the address isn't already in the list */
  for (dwIndex = 0;  dwIndex < dwConfirmationCount;  dwIndex++) {
    if (CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE,
                      pConfirmations[dwIndex].lpszAddress, -1,
                      lpszAddress, -1) =  = CSTR_EQUAL) return FALSE;
  }
   
  if (dwConfirmationCount =  = dwConfirmationSize) {
    if (!pConfirmations)
      pTemp = (SPC_CONFIRMATION *)LocalAlloc(LMEM_FIXED, sizeof(SPC_CONFIRMATION));
    else
      pTemp = (SPC_CONFIRMATION *)LocalReAlloc(pConfirmations,
          sizeof(SPC_CONFIRMATION) * (dwConfirmationSize + 1), 0);
    if (!pTemp) return FALSE;
    pConfirmations = pTemp;
    dwConfirmationSize++;
  }
   
  pConfirmations[dwConfirmationCount].lpszAddress = (LPTSTR)LocalAlloc(
                      LMEM_FIXED, sizeof(TCHAR) * (lstrlen(lpszAddress) + 1));
  if (!pConfirmations[dwConfirmationCount].lpszAddress) return FALSE;
  lstrcpy(pConfirmations[dwConfirmationCount].lpszAddress, lpszAddress);
   
  pConfirmations[dwConfirmationCount].lpszID = (LPSTR)LocalAlloc(LMEM_FIXED,
                      lstrlenA(lpszID) + 1);
  if (!pConfirmations[dwConfirmationCount].lpszID) {
    LocalFree(pConfirmations[dwConfirmationCount].lpszAddress);
    return FALSE;
  }
  lstrcpyA(pConfirmations[dwConfirmationCount].lpszID, lpszID);
   
  /* File Times are 100-nanosecond intervals since January 1, 1601 */
  GetSystemTimeAsFileTime((LPFILETIME)&liExpire);
  liExpire.QuadPart += (SPC_CONFIRMATION_EXPIRE * (__int64)10000000);
  pConfirmations[dwConfirmationCount].liExpire = liExpire;
   
  dwConfirmationCount++;
  return TRUE;
}
   
BOOL SpcConfirmationCreate(LPCTSTR lpszAddress, LPSTR *lpszID) {
  BYTE pbBuffer[16];
   
  if (!spc_rand(pbBuffer, sizeof(pbBuffer))) return FALSE;
  if (!(*lpszID = (LPSTR)spc_base64_encode(pbBuffer, sizeof(pbBuffer), 0)))
    return FALSE;
  if (!NewConfirmation(lpszAddress, *lpszID)) {
    LocalFree(*lpszID);
    return FALSE;
  }
  return TRUE;
}
   
BOOL SpcConfirmationReceive(LPCTSTR lpszAddress, LPCSTR lpszID) {
  DWORD         dwIndex;
  LARGE_INTEGER liNow;
   
  /* Before we check the pending list of confirmations, prune the list to
   * remove expired entries.
   */
  GetSystemTimeAsFileTime((LPFILETIME)&liNow);
  for (dwIndex = 0;  dwIndex < dwConfirmationCount;  dwIndex++) {
    if (pConfirmations[dwIndex].liExpire.QuadPart <= liNow.QuadPart) {
      LocalFree(pConfirmations[dwIndex].lpszAddress);
      LocalFree(pConfirmations[dwIndex].lpszID);
      if (dwConfirmationCount > 1 && dwIndex < dwConfirmationCount - 1)
        pConfirmations[dwIndex] = pConfirmations[dwConfirmationCount - 1];
      dwIndex--;
      dwConfirmationCount--;
    }
  }
   
  for (dwIndex = 0;  dwIndex < dwConfirmationCount;  dwIndex++) {
    if (CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE,
                      pConfirmations[dwIndex].lpszAddress, -1,
                      lpszAddress, -1) =  = CSTR_EQUAL) {
      if (lstrcmpA(pConfirmations[dwIndex].lpszID, lpszID) != 0) return FALSE;
      LocalFree(pConfirmations[dwIndex].lpszAddress);
      LocalFree(pConfirmations[dwIndex].lpszID);
      if (dwConfirmationCount > 1 && dwIndex < dwConfirmationCount - 1)
        pConfirmations[dwIndex] = pConfirmations[dwConfirmationCount - 1];
      dwConfirmationCount--;
      return TRUE;
    }
  }
  return FALSE;
}

8.22.4 See Also

Recipe 11.2