Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"Escape Unicode Characters",
"Unescape Unicode Characters",
"Normalise Unicode",
"To Fullwidth",
"To Quoted Printable",
"From Quoted Printable",
"To Punycode",
Expand Down Expand Up @@ -591,4 +592,4 @@
"Comment"
]
}
]
]
57 changes: 57 additions & 0 deletions src/core/operations/ToFullwidth.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* @author jyeu [chen@jyeu.xyz]
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/

import Operation from "../Operation.mjs";

/**
* To Fullwidth operation
*/
class ToFullwidth extends Operation {
/**
* ToFullwidth constructor
*/
constructor() {
super();

this.name = "To Fullwidth";
this.module = "Encodings";
this.description =
"Converts ASCII (halfwidth) characters to their fullwidth Unicode equivalents (U+FF01–U+FF5E). Commonly used in security testing to bypass WAF keyword filters, evade regex-based blocklists, and exploit Unicode normalization (NFKC/NFKD) vulnerabilities in web applications and path parsers. For example, <code>/admin</code> may bypass a WAF rule matching <code>/admin</code> if the backend normalises Unicode before routing.";
this.infoURL =
"https://wikipedia.org/wiki/Halfwidth_and_fullwidth_forms_(Unicode_block)";
this.inputType = "string";
this.outputType = "string";
this.args = [];
}

/**
* @param {string} input
* @param {never} _args
* @returns {string}
*/
run(input, _args) {
let output = "";

for (const char of input) {
const code = char.codePointAt(0);

if (code === 0x20) {
// Regular space -> Ideographic space (U+3000)
output += "\u3000";
} else if (code >= 0x21 && code <= 0x7e) {
// Visible ASCII characters -> Fullwidth equivalents (U+FF01–U+FF5E)
output += String.fromCodePoint(code + 0xfee0);
} else {
// Non-ASCII characters (CJK, newlines, etc.) are passed through unchanged
output += char;
}
}

return output;
}
}

export default ToFullwidth;
1 change: 1 addition & 0 deletions tests/operations/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ import "./tests/TakeNthBytes.mjs";
import "./tests/Template.mjs";
import "./tests/TextEncodingBruteForce.mjs";
import "./tests/TextIntegerConverter.mjs";
import "./tests/ToFullwidth.mjs";
import "./tests/ToFromInsensitiveRegex.mjs";
import "./tests/TranslateDateTimeFormat.mjs";
import "./tests/Typex.mjs";
Expand Down
144 changes: 144 additions & 0 deletions tests/operations/tests/ToFullwidth.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* To Fullwidth tests.
*
* @author jyeu [chen@jyeu.xyz]
*
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";

TestRegister.addTests([
{
name: "To Fullwidth: empty string",
input: "",
expectedOutput: "",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: lowercase letters",
input: "admin",
expectedOutput: "\uFF41\uFF44\uFF4D\uFF49\uFF4E",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: uppercase letters",
input: "ADMIN",
expectedOutput: "\uFF21\uFF24\uFF2D\uFF29\uFF2E",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: digits",
input: "0123456789",
expectedOutput: "\uFF10\uFF11\uFF12\uFF13\uFF14\uFF15\uFF16\uFF17\uFF18\uFF19",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: space becomes ideographic space (U+3000)",
input: "hello world",
expectedOutput: "\uFF48\uFF45\uFF4C\uFF4C\uFF4F\u3000\uFF57\uFF4F\uFF52\uFF4C\uFF44",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: common punctuation and symbols",
input: "!@#$%^&*()_+-=[]{}|;':\",./<>?",
expectedOutput: "\uFF01\uFF20\uFF03\uFF04\uFF05\uFF3E\uFF06\uFF0A\uFF08\uFF09\uFF3F\uFF0B\uFF0D\uFF1D\uFF3B\uFF3D\uFF5B\uFF5D\uFF5C\uFF1B\uFF07\uFF1A\uFF02\uFF0C\uFF0E\uFF0F\uFF1C\uFF1E\uFF1F",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: slash for WAF bypass simulation",
input: "/admin/secret",
expectedOutput: "\uFF0F\uFF41\uFF44\uFF4D\uFF49\uFF4E\uFF0F\uFF53\uFF45\uFF43\uFF52\uFF45\uFF54",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: non-ASCII characters pass through unchanged",
input: "你好世界",
expectedOutput: "你好世界",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: newline passes through unchanged",
input: "line1\nline2",
expectedOutput: "\uFF4C\uFF49\uFF4E\uFF45\uFF11\n\uFF4C\uFF49\uFF4E\uFF45\uFF12",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: mixed ASCII and non-ASCII",
input: "hello,世界!",
expectedOutput: "\uFF48\uFF45\uFF4C\uFF4C\uFF4F\uFF0C世界\uFF01",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: boundary character 0x21 (!)",
input: "!",
expectedOutput: "\uFF01",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
{
name: "To Fullwidth: boundary character 0x7E (~)",
input: "~",
expectedOutput: "\uFF5E",
recipeConfig: [
{
op: "To Fullwidth",
args: [],
},
],
},
]);