Bypassing Filters with JSFuck: When Character Restrictions Aren't Enough

XSS Series - Part 4 Part 4 of 6

After learning the basics of XSS and practicing with beginner challenges, you might think that filtering out dangerous characters would be enough to prevent XSS attacks. Unfortunately, security is rarely that simple. In this article, we’ll explore a challenge that demonstrates why character-based filtering can be bypassed using an esoteric JavaScript encoding technique called JSFuck.

The challenge we’ll be working with is the “NO Alphabet Challenge” from xss.challenge.training.hacq.me. This challenge filters out all alphanumeric characters (a-zA-Z0-9), which might seem like it would prevent any JavaScript execution. However, as we’ll see, there are creative ways to work around such restrictions.

The Challenge: NO Alphabet

Challenge URL: easy01.php

Let’s examine what this challenge does:

<script src="hook.js"></script>
<?php
// by escaping the payload you won't break this system, haha! :-)
$escaped = preg_replace("/[a-zA-Z0-9]/", "", $_GET['payload']);
?>

<script>
    // here you can inject an arbitrary script,
    // but I guess you can't do anything, cuz the script can't include a-zA-Z0-9 ! :-)
    <?= $escaped ?>
</script>

Understanding the Filter

The code uses PHP’s preg_replace() function to remove all alphanumeric characters from the user input:

$escaped = preg_replace("/[a-zA-Z0-9]/", "", $_GET['payload']);

This regular expression /[a-zA-Z0-9]/ matches:

  • All lowercase letters (a-z)
  • All uppercase letters (A-Z)
  • All digits (0-9)

Any character matching this pattern is removed from the input, leaving only special characters like []()!+, etc.

The Problem

At first glance, this filter seems effective. After all, JavaScript code typically contains letters and numbers. How can you write alert('XSS') without using the letters ‘a’, ‘l’, ‘e’, ‘r’, ‘t’, ‘X’, ‘S’ or quotes?

Filter Bypass Flow

The diagram above shows the flow: user input goes through the filter, which removes all alphanumeric characters. However, by using JSFuck encoding, we can create valid JavaScript code using only the characters that pass through the filter.

What is JSFuck?

JSFuck is an esoteric programming style for JavaScript. It’s a subset of JavaScript that uses only six characters: []()!+. Despite this extreme limitation, JSFuck can represent any JavaScript program.

How JSFuck Works

JSFuck works by exploiting JavaScript’s type coercion and the fact that you can access properties and methods using bracket notation. Here’s how it builds up from basic values:

Basic Values

JavaScriptJSFuckExplanation
false![]Negating an empty array
true!![]Double negation of an empty array
undefined[][[]]Accessing non-existent property
NaN+[![]]Converting false to number
0+[]Converting empty array to number
1+!+[]Converting true to number
2!+[]+!+[]Adding two ones
10[+!+[]+[+[]]]String “10”

Building Strings

Strings are constructed character by character. For example, to get the letter ‘a’:

  • false![]
  • Convert to string: ![]+[]"false"
  • Access first character: (![]+[])[+[]]"f"

This process is repeated for each character needed, which is why JSFuck-encoded code can become very long.

Accessing Functions

Functions are accessed through array methods:

JavaScriptJSFuckExplanation
Array[]Empty array
Function[]["filter"]Accessing filter method
eval[]["filter"]["constructor"]("CODE")()Constructor function that executes code
window[]["filter"]["constructor"]("return this")()Accessing global scope

Why JSFuck Works for This Challenge

The filter removes all alphanumeric characters, but JSFuck only uses:

  • [ and ] - square brackets
  • ( and ) - parentheses
  • ! - logical NOT
  • + - unary plus or addition

None of these characters are alphanumeric, so they all pass through the filter unchanged!

The Solution

To solve the challenge, we need to encode JavaScript code in JSFuck. For demonstration purposes, we can use alert(1) (which is shorter), but alert('XSS') would work the same way - it would just be longer. The encoded payload for alert(1) is:

[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[+!+[]+[!+[]+!+[]+!+[]]]+[+!+[]]+([+[]]+![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[!+[]+!+[]+[+[]]])()

This looks completely incomprehensible, but when executed, it calls alert(1). Note: While we use alert(1) here for brevity, the same technique works with alert('XSS') - it would just generate a longer payload.

How to Generate JSFuck Code

You don’t need to manually construct JSFuck code. Tools like jsfuck.com can convert regular JavaScript code into JSFuck format. Simply paste your JavaScript code, and it will generate the JSFuck-encoded version.

Optimizing Payload Length

One significant challenge with JSFuck is that the encoded code becomes extremely long. For example:

  • alert("XSS")10,823 characters in JSFuck
  • alert(document.domain)~3,000 characters in JSFuck

JSFuck Length Comparison

The diagram above shows the dramatic difference in payload length. Why is alert(document.domain) so much shorter?

Why alert(document.domain) is Better

String Literals are Expensive

When you use alert("XSS"), JSFuck must construct the string "XSS" character by character:

  • Each character (‘X’, ‘S’, ‘S’) must be built from scratch
  • This requires many operations and results in thousands of characters

Object Properties are Shorter

When you use alert(document.domain):

  • document is a global object (accessible via window)
  • domain is a property of document
  • No string construction is needed
  • The code is approximately 70% shorter

Practical Implications

The length difference matters for several reasons:

  1. URL Length Limits: Browsers and servers have URL length limits (typically 2,048 characters). A 10,000+ character payload might be truncated.

  2. Input Field Limits: Some applications limit input field length, which could truncate long payloads.

  3. Detection: Shorter payloads are less likely to be flagged by security systems.

  4. Reliability: Shorter payloads are more likely to execute completely without being cut off.

JSFuck Conversion Comparison

Original CodeJSFuck LengthWhy It’s Long/Short
alert("XSS")~10,823 charsString literal requires character-by-character construction
alert(document.domain)~3,000 charsUses object properties, no string construction
alert(1)~1,200 charsSimple numeric value, minimal construction (used for brevity)
alert('XSS')~10,823 charsString literal requires character-by-character construction
prompt(1)~1,500 charsSimilar to alert, but function name is longer

Key Insight: Avoid string literals in JSFuck payloads whenever possible. Use object properties, numbers, or other values that don’t require string construction.

Security Lessons

This challenge teaches several important security lessons:

1. Character Filtering is Not Enough

Simply removing certain characters doesn’t prevent XSS attacks. Attackers can use encoding techniques, alternative syntax, or esoteric programming styles to bypass filters.

Better Approach: Use proper output encoding based on context (HTML encoding, JavaScript encoding, URL encoding, etc.)

2. Defense in Depth

Relying on a single security control (like character filtering) is risky. Multiple layers of defense are more effective:

  • Input validation
  • Output encoding
  • Content Security Policy (CSP)
  • Proper use of secure APIs (e.g., textContent instead of innerHTML)

3. Understand the Attack Surface

To defend against attacks, you need to understand how they work. Learning about techniques like JSFuck helps you:

  • Recognize when filters are insufficient
  • Implement better security controls
  • Test your defenses against advanced techniques

4. Context Matters

The same filtering approach won’t work in all contexts. A filter that removes alphanumeric characters might seem effective, but it doesn’t account for:

  • Encoding techniques (JSFuck, Base64, Unicode, etc.)
  • Alternative syntax
  • DOM manipulation
  • Event handlers

Filter Bypass Strategies Comparison

StrategyCharacters UsedUse CaseLengthDetection Difficulty
JSFuck[]()!+Alphanumeric filterVery longMedium
Unicode encodingVarious UnicodeCharacter restrictionsMediumLow
HTML entities&#xNN;HTML contextMediumLow
Event handlersonerror, onloadScript tag filterShortLow
Template literalsBackticksString restrictionsMediumMedium

Key Takeaways

  1. Character filtering alone is insufficient: Attackers can use encoding techniques to bypass filters.

  2. JSFuck demonstrates filter limitations: Using only 6 characters, any JavaScript can be encoded.

  3. Payload optimization matters: Using object properties instead of string literals can reduce payload size by 70%.

  4. Defense requires multiple layers: Don’t rely on a single security control.

  5. Context-aware protection: Different contexts require different protection strategies.

  6. Understanding attacks improves defense: Learning bypass techniques helps build better defenses.

Next Steps

Now that you understand how JSFuck can bypass character filters, you can:

  1. Try the challenge yourself: easy01.php
  2. Experiment with jsfuck.com to see how different code translates
  3. Practice optimizing payloads (try alert(document.domain) vs alert("test"))
  4. Learn about other encoding techniques (Base64, Unicode, HTML entities)
  5. Study Content Security Policy (CSP) as a more effective defense

Remember: The goal isn’t just to bypass filters - it’s to understand why simple filtering fails and how to implement proper security controls. Every bypass technique you learn helps you build more secure applications.

Related Articles