Compatibility

Does Obfuscation Break Form Fields and FormData?

A form field name is the oldest contract on the web. You write name="email" in the HTML, the browser puts email= in the request body, and something on the server reads a key called email. Three parties, one string, and only one of them is your JavaScript bundle -- which is precisely why it is worth measuring what happens when that bundle is protected.

What was measured

One sample file, driven through five protection profiles for the base column and through both member-renaming profiles for the rest. It takes an encoded body of the shape a browser produces from a form, turns it into an object the way every handler does, reads the fields as properties, runs a required-field check driven by a table of messages, evaluates a checkbox, and finally builds the outbound body and a FormData that a fetch call would carry.

Nothing in the sample authors the wire keys. They arrive as text -- email=ada%40example.com&plan=pro&seats=3&coupon=&notify=on -- and become object properties at runtime, which is not a rename site. That is the whole basis of the measurement. A form module that builds its own field names in the same file proves only that renaming is internally consistent, which was never in doubt. Here the names cross a real boundary in both directions: a browser wrote them from your markup, and a server will read them back.

The base column is clean. On the default target, the modern target, the gate profile, the modern gate profile and the string-transform profile, the sample reproduced the unprotected output exactly: every field value, the nested read, the required-field verdict, the checkbox, the encoded body and the FormData keys. If you protect with a default configuration, form handling is not a surface you need to think about.

Every field reads undefined, and the request body follows

With member renaming on and a pattern matching email, the sample printed email=undefined where the original printed the submitted address, and nested-email=undefined for the same value read one level down through a container. The emitted file reads posted._0x1; the browser wrote email; there is no such property, so the read produces undefined.

The line that matters is the one that leaves the process. The outbound body went from wire=email=ada%40example.com&plan=pro&seats=3 to wire=_0x1=undefined&plan=pro&seats=3. Read that carefully, because it is worse than a missing value: the request now carries a field the server has never heard of, whose value is the four-character string undefined, and the field the server does require is absent entirely. A server-side validator will reject it, and the rejection will name a field nobody can find in the HTML.

The arm matching seats and plan shows the numeric variant. The body went to _0x2=undefined&_0x1=NaN, because the seat count is passed through Number() before it is sent and Number(undefined) is NaN. A quantity that arrives as the literal text NaN is the kind of value that gets stored rather than rejected, because the column is a string in more schemas than anyone would like to admit.

One detail cuts the other way and is worth knowing. The FormData keys were unchanged at fd-keys=email,seats in every arm, because those are built with string arguments -- fd.append('email', ...) -- and a string is not an identifier. The key survives; the value read into it does not, so fd-email read undefined while the key beside it stayed correct. That split is the string rule this site documents elsewhere, showing up in the middle of a form submission.

The user is told that _0x1 is required

The required-field check is where this stops being an engineering detail and becomes something a customer sees. The sample holds a table of messages keyed by field name, walks its keys, and reports which of them the submission is missing. Unprotected, it printed missing=none. Under the pattern matching email, it printed missing=_0x1. Under the pattern matching two fields, it printed missing=_0x2,_0x1.

The mechanism is the asymmetry that runs through every one of these measurements. The table of messages is authored in your source, so its keys are renamed. The submitted object's keys came from the browser, so they are not. The check asks the submission for a key that only exists in the renamed source, does not find it, and reports the field as missing -- using the generated name, because that is the only name it has.

What the user sees depends on how your form renders that list, and every rendering is bad. A field-level message attaches to nothing, because no input has that name. A summary at the top of the form says that _0x1 is required. The form cannot be submitted successfully no matter what is typed into it, since the field the check wants does not exist in the markup at all. This is the rare member-renaming failure that is loud in the product and silent in the logs.

A ticked checkbox becomes an unticked one

Checkboxes have a shape that turns a renamed read into a specific, quiet wrong answer. A browser sends nothing at all for an unticked box, and name=on for a ticked one, so the standard test is a comparison against a string. With a pattern matching that field name, the sample went from notify=true to notify=false.

There is no error and no missing value in any log. The read produces undefined, the comparison to 'on' is false, and false is exactly what an unticked box produces, so every downstream consumer receives a well-formed answer. The record says the user did not tick the box. The user did tick the box.

Which way that cuts depends entirely on what the box means, and this is the general rule this site keeps arriving at from different directions: the absent state of a boolean is the state you get, and it is not always the harmless one. A marketing opt-in that silently reads as false is a lost lead. A consent checkbox that reads as false may block a signup. A checkbox whose ticked meaning is do not do something -- the unsubscribe box, the do-not-share box, the skip-the-email box -- reads as false and the thing the user asked you not to do happens.

Two ways to read a field, and only one of them is a rename site

The sample reads its fields in both of the shapes that appear in real handlers, and the arms separate them cleanly. A property read -- posted.email -- is an identifier in the emitted code and is rewritten. A read by string literal is not an identifier at all, which is the documented rule on this site and the reason the FormData keys built with fd.append('email', ...) came through every arm unchanged.

The required-field loop is the case that stops the rule being a simple one, and it is worth following closely because it is the most common shape in validation code. That loop reads by string: it walks a table of field names and looks each one up on the submission. It still failed. The strings it looks up are produced from the keys of a table authored in your source, so they are the renamed names, delivered to a bracket lookup at runtime. The lookup itself is innocent; the value handed to it is not, and the measured result was a report of a missing field called _0x1.

So the useful distinction is not dot access versus bracket access. It is whether the string reaching the lookup is one you wrote as a literal or one your program derived from its own identifiers. posted['email'] is safe for the same reason the FormData key was. posted[key], where key came from iterating an object you authored, inherits every rename that object received.

That is also the practical way to audit a form handler before you change any settings. Search for each field name as a string as well as a property. Where it appears as a quoted literal, that path is unaffected. Where the code walks its own table of fields and looks them up, that path carries the renaming with it -- and in a typical form both shapes are present, a few lines apart, which is why partial and confusing symptoms are the normal presentation.

Reading the symptom back to the cause

Because every arm here produces a distinct signature, the symptom tells you which name moved, and that is worth having written down before you need it. A field that reads undefined in the handler while the network panel shows the value arriving means the property read was renamed and the browser's key was not. Nothing else produces that combination: the data is on the wire and absent in the object.

A request body carrying a parameter you do not recognise, with the literal text undefined as its value, means the same rename reached the code that builds the outbound body. If that parameter's value is NaN instead, the field was passed through a numeric conversion first, which narrows it to the numeric fields on the form. A server-side log full of rejected requests naming a parameter nobody has ever documented is the same event seen from the other side.

A validation message that names something like _0x1 is the most specific signature of all: it means the check is iterating a table of field names authored in your source. That message can only come from the code that walks your own field list, so it points at the validation table rather than at the individual read.

A boolean that is always false, with no error anywhere, is the checkbox shape. There is no way to distinguish it from a user leaving the box empty by looking at the record, which is why it has to be tested rather than inferred. Submit the form once with the box ticked against the protected build and read the value your handler stores. If it is false, the field name was renamed, and every record written since that build shipped carries the same wrong answer.

What this means in practice

The headline is narrow and should not be inflated. Protection does not break forms. Five profiles, including both gate profiles and the string transform, reproduced the sample's output exactly. Only member renaming reaches this surface, and only when the pattern matches the field names themselves.

One arm is worth reporting because it draws the boundary. A pattern matching the container that the fields hang off -- an object this file both builds and reads -- produced no difference at all, on either target. Both ends of that contract are in the same file, so both were renamed together and stayed consistent. Renaming is only dangerous where the two ends of a name are written by different parties, and in a form there are always at least two: your markup and your server. One further arm renamed nothing at all, because the name in the pattern was a variable rather than a property, and it is recorded here as inconclusive rather than counted as a pass.

The practical rule is the same one that governs every externally supplied name. Field names belong in the reserved list, not the rename pattern, and a member pattern anchored to identifiers distinctive to your own application will not match a bare email or plan in the first place. If you want the check in one line, submit the form against the protected build and read the request body in the network panel: if a parameter name looks generated, or a value is the literal text undefined or NaN, the field was renamed. That is the only place this defect is visible, because the application itself never complains.

Frequently asked questions

Does obfuscation break HTML form submissions?

Not in the default configuration. A sample that parses an encoded form body, reads the fields as properties, runs a required-field check, evaluates a checkbox and builds an outbound body produced identical output on all five protection profiles measured. Form fields only become a surface when member renaming is switched on and the pattern matches the field names.

Why does my form field read as undefined after obfuscation?

Because the property read was renamed while the browser still writes the original name. Form keys are created from the encoded request text at runtime, which is not a rename site, so the emitted code asks the submission for a name that only exists in your source. In the measured run the field value came back undefined and the value sent onward was the literal text undefined.

What ends up in the request body when a form field is renamed?

A parameter the server has never seen. The measured body went from email=ada%40example.com&plan=pro&seats=3 to _0x1=undefined&plan=pro&seats=3. The required field is absent and a generated name is present, so a server-side validator rejects the request and names a field that does not appear anywhere in your HTML.

Can obfuscation put a generated name in front of a user?

Yes, and this is one of the few places it happens. The required-field table is authored in your source and is renamed; the submitted object is not. The check therefore reports a missing field using the only name it has, which is the generated one. The measured output went from missing=none to missing=_0x1, and no input in the markup carries that name, so the form cannot be completed.

Does renaming affect checkboxes differently?

Yes, because an unticked checkbox is represented by absence. The standard comparison against 'on' evaluates false when the read returns undefined, which is indistinguishable from the user leaving the box empty. The measured value went from true to false with nothing logged. For a do-not-contact or do-not-share box, the silent answer is the one the user was trying to prevent.

Are FormData keys renamed too?

No, when they are supplied as strings. fd.append('email', value) keeps its key because a string literal is not an identifier, and the measured FormData keys were unchanged in every arm. The value passed in is a different matter: it was read through a renamed property and arrived as undefined, so the key was right and the content was not.

How do I check my own form against a protected build?

Submit it and read the request body in the browser network panel, not the source. A parameter name that looks generated, a value that is the literal text undefined, or a numeric field that arrives as NaN each mean the field was renamed. Add the field names to the reserved list, or anchor the member pattern to names distinctive to your application so it never matches them.

Related reading