🧠 Introduction

Sometimes, the most critical vulnerabilities are not caused by complex logic… but by small inconsistencies in how input is handled.

In this write-up, I'll walk through a vulnerability in a file access system that uses HMAC-based presigned URLs, and how a subtle mismatch between validation and sanitization leads to arbitrary file access.

βš™οΈ Application Logic Overview

The application exposes two main functionalities:

  • action=sign β†’ Generates a signed URL for a file
  • action=download β†’ Downloads the file using the signed URL

Security Controls

  • Only files under the public/ directory are allowed
  • Path traversal (..) is explicitly blocked

At first glance, everything looks secure.

🚨 The Vulnerability

The issue lies in an inconsistent handling of the filename between two stages:

πŸ”Ή During Signing

  • The application:
  1. Checks if the filename contains ..
  2. Then applies sanitization via sanitizeFilename()

πŸ”Ή During Download

  • ❌ No sanitization is applied
  • The filename is used directly

πŸ’£ Exploit Strategy

The idea is simple:

Make the server validate a "safe" input… that becomes "dangerous" after sanitization.

πŸ§ͺ Step-by-Step Exploitation

πŸ”Ή Step 1 β€” Bypass the .. Filter

Instead of using:

../

We inject a control character between the dots:

.%7f.

Resulting payload:

public/.%7f./super_secret.txt

βœ” The server does NOT detect ..

βœ” The validation passes

πŸ”Ή Step 2 β€” Sanitization Effect

The function: sanitizeFilename()

removes control characters. So: .%7f. β†’ ..

Final transformed path: public/../super_secret.txt

πŸ”Ή Step 3 β€” Generate a Valid Signature

Request:

?action=sign&filename=public/.%7f./super_secret.txt

The server generates the signature based on:

public/../super_secret.txt

πŸ”Ή Step 4 β€” Retrieve the File

Now we use the normalized path:

?action=download&filename=public/../super_secret.txt&expires=…&signature=…

Resolved path:

files/public/../super_secret.txt β†’ files/super_secret.txt

βœ” Signature matches βœ” Access control bypassed βœ” Sensitive file retrieved

πŸ“Š Impact

  • Unauthorized access to restricted files
  • Complete bypass of access control
  • High-risk sensitive data exposure

This vulnerability effectively allows an attacker to read any file accessible via path traversal.

🧨 Root Cause

The root issue is:

❌ Validation is performed before sanitization

This creates a gap where input can change after validation.

πŸ›‘οΈ Remediation

βœ”οΈ Correct Approach

  1. Apply sanitization before validation
  2. Ensure consistent input handling across all flows

βœ”οΈ Additional Protections

πŸ”’ Reject control characters

if (preg_match('/[\x00-\x1F\x7F]/', $filename)) { reject(); }

πŸ”’ Enforce strict allowlist

^public/[a-zA-Z0–9._-]+$

πŸ”’ Normalize paths securely

  • Use realpath()
  • Ensure the resolved path stays within the allowed directory

πŸ”’ Use consistent logic

  • The same normalized input must be used in:
  • Signing
  • Verification

🧩 Key Takeaways

  • ⚠️ Order of operations matters in security
  • 🧠 Sanitization β‰  Validation
  • πŸ” Always normalize input before applying checks
  • πŸ’‘ Control characters can be used as powerful bypass techniques

🏁 Conclusion

This vulnerability highlights how a small inconsistency in input handling can completely break a security mechanism.

Even when strong cryptographic protections like HMAC are used, logic flaws can render them useless.