How ASTs Change the Way You Think About JavaScript

For vulnerability research and programmable fuzzing

How ASTs Change the Way You Think About JavaScript

For vulnerability research and programmable fuzzing

September 8, 2026

Is This Valid JavaScript?

Let’s analyze this JavaScript program:

It looks like strange and unusual code, which raises some interesting questions:

  • Is it valid JavaScript code?
  • How many structures are there here?
  • What possibilities and restrictions do these structures have?

When we want to understand which constructions the JavaScript language allows and how they can be combined with each other, we usually turn to different documentation, MDN Web Docs, or simply try different variants in the console.

Understanding a specific structure can be simple, but knowing everything it can contain, where it can appear, and which other structures it can be combined with is somewhat more complicated. Even a simple expression like

is made up of different properties and nodes related to each other, even though it may look like a single expression.

A different way to analyze JavaScript is through its Abstract Syntax Tree (AST). Instead of thinking only in terms of syntax, we can think in terms of nodes, types, and the relationships between them. Each node contains specific properties and fields, which define what types of nodes can appear in each position.

To study these relationships, we can use ESTree, a standard specification in programming that defines how to represent JavaScript code in an Abstract Syntax Tree (AST). ESTree defines the different types of nodes that can make up a program, along with their properties and fields, and the relationships between them.

In practice, we work with the AST representation used by Babel, which is based on ESTree. Babel documents these nodes and provides tools to extract the AST from JavaScript code, making it a reference for studying these structures: Babel Parser AST Specification.

For example, ESTree defines the ForStatement node, which represents a for loop, like this:

Which we can translate into:

First of all, we can see that ForStatement belongs to the Statement group. Its init, test, and update fields are optional because they appear as null (not to be confused with the NullLiteral node (null;), so the syntax for(;;) is perfectly valid. We can also see that the init field can contain either a variable declaration, for example for(let i = 0;;), or an expression such as for(() => new Array;;). test and update accept only expressions, for example for(; -1; (function()())).

The body is of type Statement, so it does not have to be the usual BlockStatement: for(;;). Since it is a Statement, we find other nodes that would be perfectly valid, such as WhileStatement with(), IfStatement if(true), TryStatement try , or even another ForStatement.

Therefore, for (;;) while (false); is a valid construction. The body of the ForStatement is a WhileStatement.

The fact that a node is a Statement does not mean it can appear anywhere. As we know, a ReturnStatement return; is also a Statement and is valid only within the context of a function or method.

As we can see, the AST helps us better understand the possibilities and limitations of the language’s structure.

Statements, Expressions and the Building Blocks of JavaScript

Every JavaScript program starts from a root node: Program. Its body contains an array of the program’s statements.

From there, each field in the tree defines what type of node can appear in each position. If a field accepts a Statement and the context allows it, we can use some of the nodes that belong to that category. If it accepts an Expression, we have a different set of possibilities.

For example, a Program can directly contain nodes such as IfStatement, VariableDeclaration, or FunctionDeclaration. A CallExpression func_1();, on the other hand, cannot appear directly in the body, because it belongs to the Expression category.

To represent an expression as an independent statement, there is ExpressionStatement, a node of type Statement whose expression field contains the corresponding expression.

These relationships repeat constantly. An IfStatement, for example, contains an Expression in test and accepts a Statement both in consequent and, optionally, in alternate.

Since FunctionDeclaration belongs to the Statement category, it can appear in one of those positions when the syntactic context allows it.

Structurally:

As we can see, the two main categories are Statement and Expression:

  • Within Statement, we find nodes such as ForStatement, IfStatement, DoWhileStatement, TryStatement, BlockStatement, or ExpressionStatement.
  • Within Expression, we find, among others, BinaryExpression, CallExpression, MemberExpression, ObjectExpression, FunctionExpression, or ArrowFunctionExpression.

As researchers, this helps us understand when a field accepts an Expression and start testing which other expressions can occupy that position. A good example appears in classes. ClassDeclaration uses the fields defined by ClassBase:

When we think about extends, the most common form is to imagine one class inheriting from another through its name, which would be an Identifier node:

However, as we can see in the description of the superClass field, it is an Expression rather than an Identifier, which opens up new possibilities:

A FunctionExpression, for example, can also occupy that position:

Structurally:

We can also use other expressions:

While conducting research or developing a fuzzer, this way of looking at the AST allows us to explore less common combinations and test how they end up being processed by the engine.

Seeing JavaScript as a Tree

Let’s go back to the initial example:

After what we have explained, we stop seeing three for loops written on the same line and instead represent the relationship between their body fields:

Here we can see more clearly what we have: three nested ForStatement nodes. The body of the first is a ForStatement, the body of the second is another ForStatement, and the body of the third is an EmptyStatement ;.

From there, we can expand the representation and observe the complete structure:

We now have the structure of the program described and can go into each node. The first ForStatement, for example, has an AssignmentExpression in init and a BinaryExpression in test. And that BinaryExpression, in turn, has its own structure:

This allows us to see and understand which node is inside another, what type of node can appear in each position and, therefore, which parts of the structure we could replace with others.

For example, we can slightly modify the program:

Its structure now changes to:

Now the body of the first ForStatement no longer directly contains another ForStatement, but an IfStatement. And it is its consequent that contains the second ForStatement.

We have brought this way of working with the program, following nodes, fields, and the relationships between them, into SeekZero.

SeekZero AST nodes

In SeekZero, we work directly with the nodes and their properties that make up the AST of the JavaScript program. Each one represents a part of the program and can be configured independently.

FunctionDeclaration structure in a Mutation Plan

FunctionDeclaration structure in a Mutation Plan

This allows us to work with the same vocabulary we have used throughout the article. A researcher or an LLM can talk to SeekZero in terms of ForStatement, BinaryExpression, ClassDeclaration, CallExpression, etc.

Each node also has its own fields and properties. A BinaryExpression, for example, contains left and right, as well as properties such as operator that determine which expression we are building.

For example, this expression:

can be represented in simplified form as:

If we keep the same left and right nodes but change the operator property, we can obtain expressions that are structurally very similar but behave differently:

In SeekZero, these parts can be configured independently at different levels: through the Campaign configuration, where each campaign defines its own targets and research goals, and through Mutation Plans, which apply the same level of control but to specific JavaScript nodes in the sample.

In the case of a BinaryExpression, we can decide what types of nodes we want to use in left or right, which ones we want to prioritize, or which operators we want to allow.

Configuring a BinaryExpression node

Configuring a BinaryExpression node

The same applies to mutations. A FunctionDeclaration, for example, has properties such as async or generator, in addition to its parameters and body.

This makes it possible to explore changes over the same structure. A normal function can become async, a generator, or combine both properties:

As we saw in previous posts, Mutation Plans allow us to apply this same level of control to a specific sample. We can see the nodes that make up its AST, preserve a structure we are interested in, lock nodes we do not want to modify, and individually configure the parts we want to explore.

This gives us a common language with SeekZero. We can describe an idea directly using the same elements that already exist in the AST: nodes, fields, and properties, without having to create a different representation to indicate what we want to generate or mutate.

On the other hand, LLMs can reason directly about a ForStatement, a BinaryExpression, or about what type of node can appear in a specific field. The idea is that a researcher, an LLM, and SeekZero can all work with practically the same vocabulary.

References

  • https://github.com/babel/babel
  • https://github.com/babel/babel/blob/7.x/packages/babel-parser/src/types.ts
  • https://github.com/estree/estree
  • https://developer.mozilla.org/es/docs/Web/JavaScript/Reference

How ASTs Change the Way You Think About JavaScript

For vulnerability research and programmable fuzzing

How ASTs Change the Way You Think About JavaScript

For vulnerability research and programmable fuzzing

September 8, 2026

Is This Valid JavaScript?

Let’s analyze this JavaScript program:

It looks like strange and unusual code, which raises some interesting questions:

  • Is it valid JavaScript code?
  • How many structures are there here?
  • What possibilities and restrictions do these structures have?

When we want to understand which constructions the JavaScript language allows and how they can be combined with each other, we usually turn to different documentation, MDN Web Docs, or simply try different variants in the console.

Understanding a specific structure can be simple, but knowing everything it can contain, where it can appear, and which other structures it can be combined with is somewhat more complicated. Even a simple expression like

is made up of different properties and nodes related to each other, even though it may look like a single expression.

A different way to analyze JavaScript is through its Abstract Syntax Tree (AST). Instead of thinking only in terms of syntax, we can think in terms of nodes, types, and the relationships between them. Each node contains specific properties and fields, which define what types of nodes can appear in each position.

To study these relationships, we can use ESTree, a standard specification in programming that defines how to represent JavaScript code in an Abstract Syntax Tree (AST). ESTree defines the different types of nodes that can make up a program, along with their properties and fields, and the relationships between them.

In practice, we work with the AST representation used by Babel, which is based on ESTree. Babel documents these nodes and provides tools to extract the AST from JavaScript code, making it a reference for studying these structures: Babel Parser AST Specification.

For example, ESTree defines the ForStatement node, which represents a for loop, like this:

Which we can translate into:

First of all, we can see that ForStatement belongs to the Statement group. Its init, test, and update fields are optional because they appear as null (not to be confused with the NullLiteral node (null;), so the syntax for(;;) is perfectly valid. We can also see that the init field can contain either a variable declaration, for example for(let i = 0;;), or an expression such as for(() => new Array;;). test and update accept only expressions, for example for(; -1; (function()())).

The body is of type Statement, so it does not have to be the usual BlockStatement: for(;;). Since it is a Statement, we find other nodes that would be perfectly valid, such as WhileStatement with(), IfStatement if(true), TryStatement try , or even another ForStatement.

Therefore, for (;;) while (false); is a valid construction. The body of the ForStatement is a WhileStatement.

The fact that a node is a Statement does not mean it can appear anywhere. As we know, a ReturnStatement return; is also a Statement and is valid only within the context of a function or method.

As we can see, the AST helps us better understand the possibilities and limitations of the language’s structure.

Statements, Expressions and the Building Blocks of JavaScript

Every JavaScript program starts from a root node: Program. Its body contains an array of the program’s statements.

From there, each field in the tree defines what type of node can appear in each position. If a field accepts a Statement and the context allows it, we can use some of the nodes that belong to that category. If it accepts an Expression, we have a different set of possibilities.

For example, a Program can directly contain nodes such as IfStatement, VariableDeclaration, or FunctionDeclaration. A CallExpression func_1();, on the other hand, cannot appear directly in the body, because it belongs to the Expression category.

To represent an expression as an independent statement, there is ExpressionStatement, a node of type Statement whose expression field contains the corresponding expression.

These relationships repeat constantly. An IfStatement, for example, contains an Expression in test and accepts a Statement both in consequent and, optionally, in alternate.

Since FunctionDeclaration belongs to the Statement category, it can appear in one of those positions when the syntactic context allows it.

Structurally:

As we can see, the two main categories are Statement and Expression:

  • Within Statement, we find nodes such as ForStatement, IfStatement, DoWhileStatement, TryStatement, BlockStatement, or ExpressionStatement.
  • Within Expression, we find, among others, BinaryExpression, CallExpression, MemberExpression, ObjectExpression, FunctionExpression, or ArrowFunctionExpression.

As researchers, this helps us understand when a field accepts an Expression and start testing which other expressions can occupy that position. A good example appears in classes. ClassDeclaration uses the fields defined by ClassBase:

When we think about extends, the most common form is to imagine one class inheriting from another through its name, which would be an Identifier node:

However, as we can see in the description of the superClass field, it is an Expression rather than an Identifier, which opens up new possibilities:

A FunctionExpression, for example, can also occupy that position:

Structurally:

We can also use other expressions:

While conducting research or developing a fuzzer, this way of looking at the AST allows us to explore less common combinations and test how they end up being processed by the engine.

Seeing JavaScript as a Tree

Let’s go back to the initial example:

After what we have explained, we stop seeing three for loops written on the same line and instead represent the relationship between their body fields:

Here we can see more clearly what we have: three nested ForStatement nodes. The body of the first is a ForStatement, the body of the second is another ForStatement, and the body of the third is an EmptyStatement ;.

From there, we can expand the representation and observe the complete structure:

We now have the structure of the program described and can go into each node. The first ForStatement, for example, has an AssignmentExpression in init and a BinaryExpression in test. And that BinaryExpression, in turn, has its own structure:

This allows us to see and understand which node is inside another, what type of node can appear in each position and, therefore, which parts of the structure we could replace with others.

For example, we can slightly modify the program:

Its structure now changes to:

Now the body of the first ForStatement no longer directly contains another ForStatement, but an IfStatement. And it is its consequent that contains the second ForStatement.

We have brought this way of working with the program, following nodes, fields, and the relationships between them, into SeekZero.

SeekZero AST nodes

In SeekZero, we work directly with the nodes and their properties that make up the AST of the JavaScript program. Each one represents a part of the program and can be configured independently.

FunctionDeclaration structure in a Mutation Plan

FunctionDeclaration structure in a Mutation Plan

This allows us to work with the same vocabulary we have used throughout the article. A researcher or an LLM can talk to SeekZero in terms of ForStatement, BinaryExpression, ClassDeclaration, CallExpression, etc.

Each node also has its own fields and properties. A BinaryExpression, for example, contains left and right, as well as properties such as operator that determine which expression we are building.

For example, this expression:

can be represented in simplified form as:

If we keep the same left and right nodes but change the operator property, we can obtain expressions that are structurally very similar but behave differently:

In SeekZero, these parts can be configured independently at different levels: through the Campaign configuration, where each campaign defines its own targets and research goals, and through Mutation Plans, which apply the same level of control but to specific JavaScript nodes in the sample.

In the case of a BinaryExpression, we can decide what types of nodes we want to use in left or right, which ones we want to prioritize, or which operators we want to allow.

Configuring a BinaryExpression node

Configuring a BinaryExpression node

The same applies to mutations. A FunctionDeclaration, for example, has properties such as async or generator, in addition to its parameters and body.

This makes it possible to explore changes over the same structure. A normal function can become async, a generator, or combine both properties:

As we saw in previous posts, Mutation Plans allow us to apply this same level of control to a specific sample. We can see the nodes that make up its AST, preserve a structure we are interested in, lock nodes we do not want to modify, and individually configure the parts we want to explore.

This gives us a common language with SeekZero. We can describe an idea directly using the same elements that already exist in the AST: nodes, fields, and properties, without having to create a different representation to indicate what we want to generate or mutate.

On the other hand, LLMs can reason directly about a ForStatement, a BinaryExpression, or about what type of node can appear in a specific field. The idea is that a researcher, an LLM, and SeekZero can all work with practically the same vocabulary.

References

  • https://github.com/babel/babel
  • https://github.com/babel/babel/blob/7.x/packages/babel-parser/src/types.ts
  • https://github.com/estree/estree
  • https://developer.mozilla.org/es/docs/Web/JavaScript/Reference