Cyril Kato's blog

URL Protection Through HMAC: A Practical Approach

Abstract

This article presents a practical protocol for URL protection using HMAC (Hash-based Message Authentication Code). By embedding cryptographic signatures within URLs, we can verify their integrity and authenticity without requiring clients to possess any secret information. This approach generalizes CSRF token protection beyond forms, enables secure "magic links" for passwordless authentication, and provides a simple yet robust security layer for web applications.

Introduction

Modern web applications often need to generate URLs that should be tamper-proof—whether they facilitate access to protected resources, contain state parameters that should not be modified, or represent actions that should be executed only by authorized users. The protocol described herein addresses these needs using a server-side secret and HMAC signatures embedded directly within URLs.

A key advantage of this approach is its simplicity: the client never needs to generate or forge URLs on its own, as the server handles all signature operations. This unidirectional security model, where only the server possesses the secret, dramatically reduces implementation complexity and attack vectors.

Protocol Fundamentals

Core Principles

  • The server exclusively holds the secret key
  • URLs are generated and signed by the server only
  • Clients use URLs as opaque tokens
  • Any modification to a signed URL invalidates its signature

Signature Placement Convention

Drawing inspiration from Unix-like operating systems—where files and directories prefixed with a period (e.g., /home/user/.config) are treated as hidden—we adopt a convention where the HMAC signature in URLs is prefixed with a period:

https://example.com/.abc123def456/resource/42?action=edit

In this example, .abc123def456 is the signature that authenticates the remainder of the URL path and query parameters.

Technical Implementation

URL Signing Process

  1. Start with an unsigned URL template containing a placeholder for the signature:
    https://example.com/__SIGNATURE__/resource/42?action=edit
  2. Replace the placeholder with an empty string to create a base string for HMAC calculation:
    https://example.com//resource/42?action=edit
  3. Calculate HMAC-SHA256 of the base string using the server's secret key
  4. Convert the HMAC digest to a URL-safe Base64 encoding without padding
  5. Replace the placeholder in the original URL with the encoded signature:
    https://example.com/.abc123def456/resource/42?action=edit

URL Verification Process

  1. Extract the signature component from the URL:
    signature = "abc123def456"
  2. Replace the signature in the URL with an empty string to recreate the base string:
    base_string = "https://example.com//resource/42?action=edit"
  3. Calculate HMAC-SHA256 of the base string using the server's secret key
  4. Convert the calculated HMAC to a URL-safe Base64 encoding without padding
  5. Compare the calculated signature with the extracted signature using a constant-time comparison function
  6. Accept the URL as authentic only if the signatures match exactly

Pseudo-code Implementation

The following pseudo-code demonstrates the core implementation:


# Signing function
function sign_url(url_template, secret_key, placeholder):
    # Replace placeholder with empty string for HMAC calculation
    base_string = url_template.replace(placeholder, "")

    # Calculate HMAC-SHA256
    hmac_digest = hmac_sha256(secret_key, base_string)

    # Encode as URL-safe Base64 without padding
    signature = base64_url_encode(hmac_digest).rstrip("=")

    # Replace placeholder with signature prefixed by dot
    signed_url = url_template.replace(placeholder, "." + signature)

    return signed_url

# Verification function
function verify_url(signed_url, secret_key):
    # Parse URL to extract signature
    path_components = parse_path(signed_url)

    # Find component starting with a dot
    for i, component in enumerate(path_components):
        if component.startswith("."):
            signature = component[1:]  # Remove leading dot

            # Create base string by removing signature
            path_components[i] = ""
            base_string = reconstruct_url(path_components)

            # Calculate expected signature
            expected_hmac = hmac_sha256(secret_key, base_string)
            expected_signature = base64_url_encode(expected_hmac).rstrip("=")

            # Compare signatures using constant-time comparison
            return secure_compare(signature, expected_signature)

    return false  # No signature found
                    

Applications

Generalizing CSRF Protection

While CSRF tokens are traditionally used in HTML forms to prevent cross-site request forgery, this URL protection protocol extends the same security principle to all URLs, particularly those that trigger state-changing operations:


# Controller code for generating a protected deletion link
function generate_delete_link(resource_id):
    url_template = "/.__TOKEN__/resources/{id}/delete"
    signed_url = sign_url(url_template.replace("{id}", resource_id), SECRET_KEY, "__TOKEN__")
    return signed_url

# Route handler that verifies the signature before proceeding
function handle_delete_request(request):
    if verify_url(request.url, SECRET_KEY):
        # Signature is valid, proceed with deletion
        resource_id = extract_resource_id(request.url)
        delete_resource(resource_id)
        return success_response()
    else:
        # Invalid signature
        return unauthorized_response()
                    

Passwordless Authentication

This protocol is particularly well-suited for implementing "magic links" for passwordless authentication, where possession of the link itself grants access:


# Generate a unique login link for a user
function generate_login_link(user_email):
    # Include expiration timestamp to limit validity period
    expiration = current_time() + LINK_VALIDITY_DURATION
    url_template = "/.__TOKEN__/login?user={email}&expires={exp}"

    # Replace placeholders with actual values
    url_template = url_template.replace("{email}", url_encode(user_email))
    url_template = url_template.replace("{exp}", expiration.to_string())

    # Sign the URL
    signed_url = sign_url(url_template, SECRET_KEY, "__TOKEN__")
    return base_url() + signed_url

# Handle login via magic link
function handle_magic_link_login(request):
    if not verify_url(request.url, SECRET_KEY):
        return unauthorized_response()

    # Extract parameters
    user_email = extract_parameter(request.url, "user")
    expiration = parse_timestamp(extract_parameter(request.url, "expires"))

    # Check if link has expired
    if current_time() > expiration:
        return link_expired_response()

    # Authenticate the user
    authenticate_user(user_email)
    return redirect_to_dashboard()
                    

Secure Document Sharing

The protocol enables secure document sharing links that cannot be manipulated to access unintended documents:


# Generate a document access link with specific permissions
function generate_document_link(document_id, permissions):
    url_template = "/.__TOKEN__/documents/{id}?permissions={perms}"

    # Replace placeholders
    template = url_template.replace("{id}", document_id)
    template = template.replace("{perms}", encode_permissions(permissions))

    # Sign the URL
    signed_url = sign_url(template, SECRET_KEY, "__TOKEN__")
    return base_url() + signed_url

# Document access handler
function handle_document_access(request):
    if not verify_url(request.url, SECRET_KEY):
        return unauthorized_response()

    # Extract parameters
    document_id = extract_document_id(request.url)
    permissions = decode_permissions(extract_parameter(request.url, "permissions"))

    # Apply permissions
    document = fetch_document(document_id)
    return render_document(document, permissions)
                    

Security Considerations

Secret Key Management

The security of this protocol fundamentally depends on proper secret key management:

  • The secret key should have high entropy (at least 256 bits)
  • Rotation policies should be implemented to periodically change the key
  • The key should never be exposed to clients or third parties
  • Different applications or environments should use different keys

Replay Protection

Since signed URLs can be reused, consider implementing these additional measures for sensitive operations:

  • Include expiration timestamps in the URL (and verify them during validation)
  • Use single-use nonces for particularly sensitive operations
  • Include user session identifiers for user-specific URLs

Transport Security

Always use HTTPS when transmitting signed URLs to prevent interception and potential replay attacks. Even with signature validation, exposure of signed URLs in transit represents a security risk.

Conclusion

The URL protection protocol described in this article provides a robust yet simple mechanism for ensuring URL integrity and authenticity. By leveraging HMAC signatures and keeping secrets server-side, it minimizes implementation complexity while maximizing security benefits.

This approach is particularly valuable for:

  • Protecting against CSRF attacks across all URL-based operations
  • Implementing secure "magic links" for passwordless authentication
  • Creating tamper-proof resource-sharing URLs
  • Building secure API endpoints that are resistant to parameter tampering

As with any security mechanism, this protocol should be part of a layered security strategy, complemented by appropriate transport security (HTTPS), proper secret management, and additional contextual security measures based on the sensitivity of protected resources.

References

  1. Krawczyk, H., Bellare, M., & Canetti, R. (1997). HMAC: Keyed-Hashing for Message Authentication. RFC 2104.
  2. OWASP. (2024). Cross-Site Request Forgery Prevention Cheat Sheet. OWASP Cheat Sheet Series.
  3. Stallings, W. (2017). Cryptography and Network Security: Principles and Practice (7th ed.). Pearson.
  4. Ferguson, N., Schneier, B., & Kohno, T. (2010). Cryptography Engineering: Design Principles and Practical Applications. Wiley.