Input Validation as a Security Layer

Input validation is one of the most effective habits developers can adopt to improve software security. While it should never be the sole defense against injection attacks, it plays a crucial role in the defense in depth principle—the idea that multiple layers of security controls provide redundancy, so if one layer fails, another may still prevent exploitation.

Proper output encoding and parameterized queries remain the primary defenses against injection. However, validating all untrusted user input can make vulnerable code significantly harder to exploit by limiting what an attacker can submit. Even when data flows through a validated path, the code may still be flagged as a vulnerability—a latent risk that could become exploitable if the code changes or a new code path is introduced. In the present moment, input validation can be the difference between a theoretical weakness and an actual exploitable bug.

Input validation can be approached in several ways:

  • Allow lists or deny lists
  • Validation or sanitization
  • Server-side or client-side checks

Allow Lists vs. Deny Lists

When designing input fields, ask whether you truly need unlimited length and every possible character. Ideally, restrict input to a limited set of allowed characters with a maximum length. For example, a phone number could be constrained to a plus sign followed by 9 to 12 digits:

@Pattern(regexp="\\+\\d{9,12}")
String phoneNumber;

The next example sets expected value ranges and mandatory fields in ASP.NET Core:

public class Movie
{
    [Required]
    [StringLength(100)]
    public string Title { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; }

    [Range(0, 999.99)]
    public decimal Price { get; set; }
}

Some inputs, however, are difficult to constrain with an allow list. Email addresses, for instance, can be quite flexible according to IETF standards and RFCs 5322 and 6854. The address " "@example.org is technically valid yet could be used to inject a malicious payload.

A deny list—blocking specific disallowed characters—is a weaker defense than an allow list, but it can still protect against many attacks. There is little reason to permit characters like >, <, or " in a username. In both allow list and deny list approaches, invalid input should be simply rejected.

Validation vs. Sanitization

Sanitization, which attempts to fix invalid input by removing or replacing characters, is the least effective technique—though still better than nothing. Attackers often find ways to bypass it. Consider an attempt to prevent path traversal by stripping all occurrences of ../ from a file name (note: this is not the recommended primary defense against path traversal). An attacker could submit abc/....//xyz, which after sanitization becomes abc/../xyz, defeating the protection.

Server-Side vs. Client-Side Validation

Validation serves more than one purpose. Its primary job is often to improve user experience, which is why client-side validation is commonly used. But client-side validation is always bypassable—by attackers or even by users who want to interact with your service differently than intended. The security role of validation lives entirely on the server side.

The guiding principle is simple: never trust the client. Unless you explicitly unit test validation routines—which is recommended—they may never execute in a test environment. These checks are not meant to catch ordinary bugs (though they might); they are there to detect and prevent exploitation of security vulnerabilities.

Automating Validation Checks

Lightweight static analysis can enforce validation rules and identify anti-patterns. A semantic CodeQL query can be tailored to your usage patterns and integrated directly into your CI/CD pipeline, such as through GitHub code scanning. The following example flags potentially untrusted Spring Controller input parameters that lack validation annotations:

import semmle.code.java.dataflow.FlowSources

class SpringServletInputParameterSource extends RemoteFlowSource {
    SpringServletInputParameterSource() {
      this.asParameter() = any(SpringRequestMappingParameter srmp | 
                                                    srmp.isTaintedInput())
    }

    override string getSourceType() {
        result = "Spring servlet input parameter" }
  }

from SpringServletInputParameterSource c
where not c.asParameter()
                   .getAnAnnotation()
                   .getType()
                   .hasQualifiedName("javax.validation", "Valid")
select c

For more on CodeQL and writing semantic queries, see the CodeQL documentation.

The Takeaway

Restricting the input data an attacker can supply reduces the application's attack surface and is a powerful technique. However, it should complement—not replace—proper output encoding and parameterized queries as the primary defense against injection attacks.