A security check can inspect the right data and still reach the wrong decision if another component changes that data afterward. A path may be validated before path resolution removes .. segments. A percent-encoded value may pass a character check and then gain different characters when a later layer decodes it. Two names that look different to one component may be treated as equivalent by another.
The consequence is a representation mismatch: the security decision is made about one form of a value, while the sensitive operation uses another. An attacker does not need to defeat the policy itself if they can make the validator and the consumer disagree about what the input means.
The defensive principle is simple: define the representation your application actually uses for the security-sensitive operation, convert input to that representation at a controlled boundary, and make the security decision on that result. This article explains how to apply that principle without turning “normalization” into an unsafe instruction to decode arbitrary input repeatedly.
Security checks must describe the value that will be used
Consider a service that reads reports from one directory. The intended rule is:
Only files inside the report directory may be read.Suppose the application first checks whether a supplied path appears to start inside that directory, then asks the filesystem API to resolve the path. The order is important because path resolution can change the meaning of the string.
A simplified example is:
requested: /srv/reports/../private/settings.txt
checked: /srv/reports/../private/settings.txt
resolved: /srv/private/settings.txtThe first string contains the expected prefix. The resolved path does not refer to a file inside /srv/reports.
The important lesson is not “search for ...” That would make the defense depend on recognizing every alternate spelling that a parser or filesystem might accept. The stronger rule is:
interpret input
|
v
canonical representation
|
v
security decision
|
v
use that same decision and representationHere, canonical representation means the form chosen by the application so equivalent inputs are interpreted consistently for a particular decision. The exact operation is context-specific. Filesystem path resolution, URL parsing, character decoding, identifier case rules, and Unicode normalization are different mechanisms and should not be treated as one generic transformation.
The threat is disagreement between components
This control matters when untrusted input crosses components that can interpret it differently.
The threat model includes a caller who can choose an alternate representation that the first component accepts but a later component interprets as a value the policy should have rejected. The relevant boundary might be between an HTTP parser and application code, between application code and a filesystem API, or between two services with different identifier rules.
Normalization reduces this risk only when the application understands the transformations that actually occur. It does not protect against a policy that is wrong, an authorization check that is missing, or a compromised component that deliberately ignores the decision.
It also does not mean every input should be converted as much as possible. Unnecessary transformations can create new ambiguity. The goal is to remove ambiguity required by the protocol or domain, not to invent a universal “clean” form.
Decode at the layer that owns the encoding
A useful way to reason about transformations is to ask which layer owns each encoding.
For example, an HTTP framework may already parse the request target and percent-decode a route parameter according to its rules. Application code should know whether it receives the encoded form or the decoded form before applying validation. Blindly decoding the value again can be incorrect because the second decoding step may give a previously literal sequence a new meaning.
So avoid a vague rule such as:
decode until nothing changesPrefer an explicit pipeline:
transport syntax
|
protocol parser performs defined decoding
|
application receives one documented representation
|
application applies domain-specific normalization if required
|
validate
|
useEach transformation should have a reason. If a field is defined as an opaque identifier, the correct application-level normalization may be no normalization at all. If a username is deliberately case-insensitive, the application needs one documented comparison rule and should apply it consistently when enforcing uniqueness and identity decisions.
Normalize before validation when normalization changes meaning
Validation should normally operate on the representation that downstream sensitive code will interpret.
Imagine a field whose protocol permits percent-encoding. If the application checks the encoded text for forbidden separators and a later component decodes the text, validation has described the wrong representation. A separator introduced by the required decoding step was never considered by the policy.
The safer order is:
receive
-> perform the one expected decoding step
-> reject malformed or unexpected representation
-> validate the resulting value against its domain
-> pass that value to the consumerThis is especially important for allowlists. An allowlist is meaningful only if it describes the value that the sensitive consumer sees. Applying an allowlist to an earlier representation and then transforming the accepted value weakens the relationship between the check and the operation it is supposed to protect.
When possible, design interfaces so application code receives already-parsed values rather than raw syntax. A typed identifier or parsed path component gives later code fewer opportunities to reinterpret the original text.
File paths need containment, not string resemblance
Filesystem paths are a common place to see this principle clearly.
Suppose a report service must read only files below a configured base directory. The important security question is not whether the raw string looks harmless. It is whether the path the filesystem will use remains within the intended base after the relevant resolution rules have been applied.
A defensive design can avoid user-controlled paths entirely by mapping an opaque report ID to a server-owned path:
request: report_id = 4812
|
v
server lookup: 4812 -> stored report locationThis is simpler because the caller does not control filesystem syntax.
If the application genuinely must accept path-like input, use the platform’s path APIs rather than handwritten string replacement. Resolve according to the required filesystem semantics and enforce containment using path-aware comparison. A raw string prefix check is not a general containment test: names such as /srv/reports-old can share a textual prefix with /srv/reports without being its child.
Symbolic links and similar filesystem features add another boundary condition. Lexical normalization can remove . and .. segments without proving where a filesystem object ultimately points. Applications whose security depends on the final filesystem target need platform-appropriate resolution and containment controls, and should limit the operating-system privileges of the process as defense in depth.
Identity normalization must match uniqueness rules
Representation mismatches also occur without file paths.
Suppose an application treats account names as case-insensitive during login but performs account creation with a case-sensitive uniqueness check. Two records that storage considers different may later collapse to the same login identity.
The security-relevant property is not “lowercase every username.” The property is:
The rule used to decide whether two identities are equal
must agree with the rule used to enforce their uniqueness.The correct normalization depends on the identifier. Email addresses, internationalized domain names, filesystem names, and application-defined usernames have different standards and operational constraints. Do not borrow normalization rules from one domain and apply them automatically to another.
When defining a new application identifier, a deliberately narrow syntax can make the problem much easier. If an internal tenant key only needs lowercase ASCII letters, digits, and hyphens, accepting exactly that syntax removes many equivalent-representation questions. Human-facing names can remain separate display data.
Keep the checked value and the used value together
Even correct normalization can fail if code validates one value and later reconstructs another.
For example:
raw input -> normalize -> validate
|
+--------------------------> later consumerThe consumer has bypassed the normalized value. A later refactor can therefore restore the original mismatch.
Prefer this data flow:
raw input -> normalize -> validate -> accepted value -> consumerAfter validation, pass the accepted representation forward. Do not return to the raw input unless there is a separate, non-security purpose such as preserving original display text.
For important boundaries, make the distinction visible in types or interfaces. A parser can return an AcceptedReportPath or validated identifier rather than a plain string. The name does not create security by itself, but it makes accidental reinterpretation easier to notice during review.
Reject ambiguity instead of trying to repair everything
Normalization is often described as converting many equivalent forms into one. That is useful when those forms are intentionally supported. It does not require accepting every form a caller can construct.
If the protocol permits one encoding layer, reject unexpected nested encodings rather than repeatedly decoding them. If an identifier has a small documented syntax, reject characters outside it rather than attempting to rewrite them into something acceptable. If a path parameter only needs a filename, reject directory structure rather than supporting arbitrary paths and then trying to constrain them.
This approach reduces the number of interpretations the system must keep consistent.
There is a usability trade-off. Strict input rules may reject forms users expect to work, especially for international text. The right response is to define the product’s identity and display requirements explicitly, not to silently apply aggressive normalization. Store original display text separately when necessary, while keeping security comparisons based on a documented representation.
Test equivalent representations at the boundary
A useful test asks whether two representations that the downstream consumer treats as equivalent receive the same security decision.
For a path boundary, tests should cover valid children, parent-directory resolution, absolute-path handling, separator rules relevant to the target platform, and links or filesystem indirection if the application permits them. For a decoded protocol value, test the encoded and decoded forms that the protocol explicitly supports, malformed encodings, and unexpected extra encoding layers.
Also test the opposite property: two values that must remain distinct should not collapse unexpectedly after normalization.
These tests should exercise the real parsing and consumer APIs where practical. A unit test for a regular expression cannot prove how a filesystem, URL parser, proxy, or database comparison rule will interpret the resulting value.
Operational logging can help detect rejected ambiguous input, but logs should record enough context to diagnose the validation stage without copying secrets or sensitive raw values unnecessarily.
Know what normalization does not solve
Normalization aligns representations; it does not replace other controls.
A correctly resolved path still needs authorization if different users may access different files. A normalized account identifier still needs authentication. A decoded parameter still needs context-appropriate validation, and data later inserted into SQL, HTML, shell commands, or other interpreters needs the defensive mechanism appropriate to that sink.
Defense in depth is justified when a normalization mistake would expose high-value data or privileged operations. Examples include restricting the service account’s filesystem access, isolating uploaded or generated files, applying object-level authorization, and keeping sensitive configuration outside directories the application is expected to read.
The simpler control is often preferable when you can remove the ambiguous interface entirely. An opaque ID mapped to a server-owned resource is easier to reason about than a user-supplied path. A narrow application identifier is easier to compare consistently than unrestricted text.
Conclusion
A security decision is reliable only if it describes the same value that the sensitive operation will use. When decoding, path resolution, case handling, or another normalization step can change meaning, perform the required transformation at a controlled boundary before validation and carry the accepted representation forward.
Do not normalize blindly or repeatedly. Define which component owns each transformation, reject representations the application does not need, and test the real downstream interpretation. The practical question is not “did we validate the input?” but “did we validate the exact meaning that the next trusted component will act on?”