Skip to content

CWE-103: Struts: Incomplete validate() Method Definition

Overview

An Apache Struts 1 validator form - a ValidatorForm or DynaValidatorForm subclass - overrides validate() without calling super.validate(), so every rule declared for it in validation.xml is silently dropped. The class extends a validator base specifically to get those declarative checks, and the override throws them away. The rules stay in the XML, the form still compiles, and unvalidated input reaches the Action.

MITRE's description also covers a validator form that "does not define a validate() method", so a scanner rule built from that wording will raise on one. Check that case before acting on it. A ValidatorForm subclass with no override inherits ValidatorForm.validate(), which runs the declarative rules exactly as intended, so the finding is usually a false positive. It is real when something between the subclass and ValidatorForm overrides validate() without calling super - the same defect, one class further up - so read the intermediate base class before closing it.

Struts 1 has been unmaintained since its final 1.3.10 release in 2008 and receives no security patches. Everything below is a compensating fix for legacy code, not a substitute for migrating to Struts 2, Spring MVC, or another maintained framework.

Relationship to Other CWEs

CWE-103 is a Struts-1-specific Variant. The entries below are this page, its two parents, which MITRE draws from different views, and its sibling weakness:

OWASP Classification

A05:2025 - Injection

Risk

High: Every downstream defence that assumed the form was validated is now operating on raw request data - length limits, type constraints and format rules all silently absent. What that costs depends on where the fields land: SQL injection in a query built from them, stored XSS in a rendered field, or a business-logic bypass where a negative quantity or an out-of-range state value was never supposed to be representable.

Common Vulnerable Patterns

Overriding validate() without calling super.validate()

public class RegistrationForm extends ValidatorForm {
    private String username;
    private String email;

    // VULNERABLE - overrides validate() but never calls super.validate(),
    // so every rule declared for this form in validation.xml is skipped
    public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
        ActionErrors errors = new ActionErrors();
        if (username == null || username.trim().isEmpty()) {
            errors.add("username", new ActionMessage("error.username.required"));
        }
        return errors;
    }
}

Why this is vulnerable: ValidatorForm exists to run the declarative rules in validation.xml - required fields, length limits, email and pattern checks - through super.validate(). An override without that call disables all of them, leaving only the ad hoc check the developer remembered to write. The validation.xml entries remain in the repository, so a reviewer reading the configuration concludes the field is validated when it is not.

Missing method, or a null return

public class PaymentForm extends ValidatorForm {
    private String accountNumber;
    private String amount;

    // VULNERABLE - returning null tells Struts there were no errors
    public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
        return null;
    }
}

Why this is vulnerable: Struts treats a null return, and an empty ActionErrors, as "this form is valid" - so the request proceeds to the Action with whatever was submitted. The same happens when a plain ActionForm subclass omits validate() entirely: the inherited implementation returns no errors, because a plain ActionForm has no framework rules to fall back on.

Validation disabled or unreachable in configuration

<!-- VULNERABLE - validate="false" means validate() is never invoked -->
<action path="/register"
        type="com.example.RegisterAction"
        name="registrationForm"
        scope="request"
        validate="false"/>

<!-- VULNERABLE - name mismatch: no validation.xml rules apply to this form -->
<form-bean name="registrationForm" type="com.example.RegistrationForm"/>
<!-- while validation.xml declares: <form name="RegistrationForm"> -->

Why this is vulnerable: A correct validate() method is only run if the action mapping asks for it. validate="false" skips the call altogether, which makes a perfectly written form object irrelevant. The name mismatch is quieter still: ValidatorForm looks its rules up by mapping.getAttribute(), which is the mapping's attribute where one is declared and the form-bean name otherwise, so ordinarily validation.xml's <form name="..."> has to match the form-bean name. The match is case-sensitive, and a mismatch raises nothing and produces no field errors - the form simply has no rules and the request proceeds. It is not quite undetectable: commons-validator logs Form '<name>' not found for locale '<locale>' at warning level on every such request, which is the cheapest way to find it in a running application. Both are invisible in the Java code a reviewer is most likely to read.

Secure Patterns

Call super.validate() and merge custom checks

public class RegistrationForm extends ValidatorForm {
    private String username;
    private String email;

    // SECURE - declarative rules run first, custom rules are added to the same object
    public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
        ActionErrors errors = super.validate(mapping, request);
        if (errors == null) {
            errors = new ActionErrors();
        }

        // Checks validation.xml cannot express: uniqueness, cross-field, lookups
        if (username != null && isReservedUsername(username)) {
            errors.add("username", new ActionMessage("error.username.reserved"));
        }

        return errors;  // never null
    }
}

Why this works: super.validate(mapping, request) runs every rule declared for this form in validation.xml, so the field-level checks stay centralised and consistent across the application. The override adds only what declarative rules cannot express, and merges into the returned object instead of replacing it - so neither set of errors can silently disappear.

The null guard is defensive rather than required. ValidatorForm.validate() and DynaValidatorForm.validate() both construct an ActionErrors and return it on every path, so neither ever hands back null: an empty ActionErrors is what a valid form produces. ActionForm.validate() one level further up does return null, and a project-specific base class sitting between the two may do the same - which is the case the guard actually covers.

Wire the mapping so validation actually runs

<!-- SECURE - validation is invoked, and failures have somewhere to go -->
<form-beans>
    <form-bean name="registrationForm" type="com.example.RegistrationForm"/>
</form-beans>

<action-mappings>
    <action path="/register"
            type="com.example.RegisterAction"
            name="registrationForm"
            scope="request"
            validate="true"
            input="/WEB-INF/jsp/register.jsp"/>
</action-mappings>
<!-- validation.xml - the form name must match the form-bean name exactly -->
<form-validation>
    <formset>
        <form name="registrationForm">
            <field property="username" depends="required,minlength,maxlength">
                <arg key="registrationForm.username"/>
                <var><var-name>minlength</var-name><var-value>3</var-value></var>
                <var><var-name>maxlength</var-name><var-value>32</var-value></var>
            </field>
            <field property="email" depends="required,email">
                <arg key="registrationForm.email"/>
            </field>
        </form>
    </formset>
</form-validation>

Why this works: validate="true" is what causes Struts to call the method at all. The attribute already defaults to true, so omitting it validates too; writing it out states that the choice was made rather than inherited, which is worth doing on a mapping where someone previously set validate="false". input gives the framework somewhere to forward when validation fails - without it, a failed validation is a 500 rather than a re-displayed form, which is the usual reason a team turns validation off again. Keeping the form-bean name and the validation.xml <form name> identical is what connects the rules to the form; nothing fails the build when the two drift apart, though commons-validator does log a warning on every request once they have.

Considerations

  • The real fix is leaving Struts 1. A codebase raising this finding is running an unmaintained web framework, which is a larger finding than this one. Repairing validate() is worth doing because it is cheap and immediate, but it should be recorded as a compensating control, with the migration tracked separately rather than closed by this fix.
  • Check the configuration before concluding the code is the problem. A correct validate() with validate="false" on the mapping, or a validation.xml form name that does not match the form-bean, validates nothing. Both are silent. Confirm whether the method, the mapping, or the name is the one actually broken before editing, because fixing the method when the mapping is the cause produces a fix that changes nothing.
  • What the fields feed decides the severity. A form whose values reach a SQL statement or an unencoded JSP is a different finding from one whose values are stored and never rendered. Restoring validation is right either way, but the downstream defences (CWE-89 parameterisation, CWE-79 output encoding) are what stop the exploit, and input validation should not be treated as their replacement.
  • The reported form is a sample. Teams that override validate() without super do it as a habit. Six shipped classes run the declarative rules: ValidatorForm, DynaValidatorForm, BeanValidatorForm, LazyValidatorForm, and ValidatorActionForm and DynaValidatorActionForm in struts-extras, so a grep built from the first two names alone misses four of them. Check every subclass of any of the six, along with every action mapping carrying validate="false", before closing the finding.
  • ValidatorActionForm keys its rules on the action path, not the form-bean name. ValidatorForm.getValidationKey() returns mapping.getAttribute(), while ValidatorActionForm overrides it to return mapping.getPath(), so those forms expect <form name="/register">. Establish which base class is in use before "correcting" a validation.xml name to match the form bean: doing that to a ValidatorActionForm turns working validation off.
  • cancellable="true" lets the client skip validation outright. On a mapping that sets it, a request carrying the org.apache.struts.taglib.html.CANCEL parameter makes RequestProcessor skip validate() altogether. It defaults to false, but of the ways validation silently does not run this is the only one the user controls, so grep the mappings for it alongside validate="false".

Testing

A re-scan only confirms that super.validate() is present. Whether the rules run depends on three files agreeing, so the test has to exercise the whole path.

  • Submit a value that only validation.xml rejects - a two-character username against minlength=3, or a malformed address against the email rule - and assert the response is the input page carrying the field-specific error, not a successful submission. This is the assertion that proves the declarative rules ran; a test using an empty field passes against the vulnerable version, because the hand-written check caught that one.
  • Submit a value that only the custom check rejects (a reserved username) and assert the same. Together the two tests prove the merge, not just the call.
  • Assert a valid submission still reaches the Action. An over-tight validation.xml rule that rejects legitimate input is the common regression when rules start running for the first time in years.
  • Rename the form in validation.xml in a scratch branch and confirm a test fails. If nothing fails, the tests are not exercising the declarative rules at all, and the silent-mismatch case would go unnoticed.

Additional Resources