Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Preventing Duplicate Form Submissions in Web Applications

Tech 1

Duplicate form submissions manifest in two distinct scenarios requiring different mitigation strategies.

The first scenario involves operational duplicates, where a user triggers multiple identical requests before the server responds to the initial one (e.g., double-clicking a button). The second scenario involves logical duplicates, where two separate but valid requests arrive sequentially, carrying identical business data that could result in unintended duplicate transactions (e.g., transferrring funds twice within a short window).

Mitigating Operational Duplicates via Token Synchronization

To prevent rapid consecutive requests, a token-based pattern can be implemented. When a form is initialized, the server generates a unique token and stores it in the user's session:

Session["ExpectedSubmitToken"] = initialToken;

Upon receiving a submission, the server validates the provided token against the session value. If they match, the request proceeds, and a new token is generated and stored for the next interaction. If a mismatch occurs, the request is rejected as a duplicate operation.

var freshToken = Guid.NewGuid().ToString();
var storedToken = Session["ExpectedSubmitToken"]?.ToString();

if (storedToken != submittedToken)
{
    Session["ExpectedSubmitToken"] = freshToken;
    return new 
    {
        Success = false,
        ErrorCode = "TOKEN_MISMATCH",
        Message = "Submission rejected. This may result from multiple rapid clicks or an expired session.",
        NextToken = freshToken
    };
}

Session["ExpectedSubmitToken"] = freshToken;
// Proceed with business logic...

Mitigating Logical Duplicates via Fingerprinting and Verification Challenges

For business data duplicates, operational token checks are insufficient since both requests are formally valid. The solution involves fingerprinting the request payload and requiring explicit user confirmation if a duplicate fingerprint is detected.

When a submission arrives, the server constructs a data fingerprint (e.g., concatenating key identifiers like recipient and sender details) and compares it against the last stored fingerprint.

var verificationStatus = ValidateTransactionUniqueness(orderData, challengeCode);
if (!verificationStatus.IsValid)
{
    return new 
    {
        Success = false,
        ErrorCode = "DUPLICATE_CHALLENGE",
        Message = verificationStatus.ErrorMessage,
        RequiredChallengeCode = verificationStatus.NewChallengeCode
    };
}

The ValidateTransactionUniqueness method evaluates the payload:

private ValidationState ValidateTransactionUniqueness(OrderPayload payload, string providedChallenge)
{
    if (string.IsNullOrWhiteSpace(payload.Recipient) || string.IsNullOrWhiteSpace(payload.Sender))
    {
        return new ValidationState { IsValid = true, Message = "Insufficient data for fingerprinting; deferring to standard validation." };
    }

    var currentFingerprint = $"{payload.Recipient}:{payload.RecipientContact}:{payload.Sender}:{payload.SenderContact}";
    var previousFingerprint = Session["LastTransactionFingerprint"]?.ToString();

    if (currentFingerprint == previousFingerprint)
    {
        var requiredChallenge = GenerateRandomDigits(4);
        var storedChallenge = Session["DuplicateChallengeCode"]?.ToString();

        if (storedChallenge == null)
        {
            Session["DuplicateChallengeCode"] = requiredChallenge;
            return new ValidationState 
            {
                IsValid = false, 
                NewChallengeCode = requiredChallenge,
                ErrorMessage = "This submission closely resembles a recent transaction. Please verify the details and enter the confirmation code to proceed."
            };
        }

        if (storedChallenge != providedChallenge)
        {
            Session["DuplicateChallengeCode"] = requiredChallenge;
            return new ValidationState 
            {
                IsValid = false, 
                NewChallengeCode = requiredChallenge,
                ErrorMessage = "Incorrect confirmation code. A new code has been generated. Please verify your transaction and enter the correct code."
            };
        }

        // Challenge passed, clear challenge state and update fingerprint
        Session["DuplicateChallengeCode"] = null;
        Session["LastTransactionFingerprint"] = currentFingerprint;
        return new ValidationState { IsValid = true, Message = "Duplicate authorized." };
    }

    // Unique transaction, update fingerprint
    Session["LastTransactionFingerprint"] = currentFingerprint;
    return new ValidationState { IsValid = true, Message = "Unique transaction." };
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.