Merge db0e08518a7d37b37a31ae6904e2a1d379e4545f into 72ca1aeec7a50f4c596d44b755a6ca71f792ffcf

This commit is contained in:
Jyeu 2026-06-03 22:42:51 +01:00 committed by GitHub
commit 5073a766fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 204 additions and 1 deletions

View File

@ -49,6 +49,7 @@
"Escape Unicode Characters", "Escape Unicode Characters",
"Unescape Unicode Characters", "Unescape Unicode Characters",
"Normalise Unicode", "Normalise Unicode",
"To Fullwidth",
"To Quoted Printable", "To Quoted Printable",
"From Quoted Printable", "From Quoted Printable",
"To Punycode", "To Punycode",

View File

@ -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+FF01U+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>/dmin</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+FF01U+FF5E)
output += String.fromCodePoint(code + 0xfee0);
} else {
// Non-ASCII characters (CJK, newlines, etc.) are passed through unchanged
output += char;
}
}
return output;
}
}
export default ToFullwidth;

View File

@ -178,6 +178,7 @@ import "./tests/TakeNthBytes.mjs";
import "./tests/Template.mjs"; import "./tests/Template.mjs";
import "./tests/TextEncodingBruteForce.mjs"; import "./tests/TextEncodingBruteForce.mjs";
import "./tests/TextIntegerConverter.mjs"; import "./tests/TextIntegerConverter.mjs";
import "./tests/ToFullwidth.mjs";
import "./tests/ToFromInsensitiveRegex.mjs"; import "./tests/ToFromInsensitiveRegex.mjs";
import "./tests/TranslateDateTimeFormat.mjs"; import "./tests/TranslateDateTimeFormat.mjs";
import "./tests/Typex.mjs"; import "./tests/Typex.mjs";

View File

@ -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: [],
},
],
},
]);