Skip to main content
interlace
Plugin: secure-codingRules

no-invisible-characters

Disallows invisible letters such as the Hangul fillers (U+3164, U+FFA0), which hide identifiers, values and comment text from code review (CWE-1007)

CWE: CWE-1007 OWASP: A08:2021 Software and Data Integrity Failures

Some characters are letters as far as JavaScript is concerned and nothing at all as far as your editor is concerned. This rule is part of eslint-plugin-secure-coding, and is the drop-in for eslint-plugin-security's detect-invisible-characters.

Quick Summary

AspectDetails
CWE ReferenceCWE-1007 (Insufficient Visual Distinction of Homoglyphs)
SeverityMedium (CVSS 5.3)
Auto-Fix❌ No — a suggestion removes the character, because only the author knows whether it was intended
CategorySecurity

Vulnerability and Risk

Vulnerability: U+3164 HANGUL FILLER renders as blank space, yet it is a valid identifier character. Placed inside a name it creates a second binding that looks exactly like the first; placed on its own it is a whole identifier that reads as whitespace. The second form is the "invisible backdoor": a hidden destructured parameter or array element that carries attacker input into a sink while the diff shows nothing.

// The second `admin` contains U+3164 between "adm" and "in" — two different variables.
const admin = false;
const admin = true;

Risk: Code review is the control, and it is defeated. Unlike Trojan Source bidi overrides, these characters reorder nothing — they simply are not drawn, so there is nothing for a reviewer to notice.

Rule Details

The rule scans the source text — identifiers, strings, template literals, regular expressions and comments — for six characters:

Code pointName
U+115FHANGUL CHOSEONG FILLER
U+1160HANGUL JUNGSEONG FILLER
U+17B4KHMER VOWEL INHERENT AQ
U+17B5KHMER VOWEL INHERENT AA
U+3164HANGUL FILLER
U+FFA0HALFWIDTH HANGUL FILLER

Each is a Unicode default-ignorable code point and a letter or mark, which is the combination that makes it dangerous: it draws as nothing and it is still legal inside a JavaScript name. eslint-plugin-security covers U+3164 and U+FFA0; the other four are the same class.

The set is a list, not a range. The neighbouring characters in each block — U+3131, U+314F, U+FFA1, the conjoining jamo that compose a syllable, visible Khmer — are real letters of real scripts, and Korean or Khmer text is never reported.

What this rule deliberately leaves to its siblings, so that one character never gets two findings:

  • bidi controls (U+202A–U+202E, U+2066–U+2069) → no-bidi-characters;
  • zero-width and other format characters (U+200B, U+2060, U+FEFF, …) in strings → no-homoglyph-identifiers;
  • variation selectors and U+200D ZWJ are not reported anywhere by default: they are a required part of ordinary emoji.

A finding in a comment gets its own message, because the risk differs: in code the character changes what runs, in a comment it changes what the reviewer reads.

Each finding offers a removeInvisibleCharacter suggestion that deletes exactly that character. It is a suggestion, not a --fix, because removing a character from an identifier renames it.

Examples

❌ Incorrect

Every example below is shown with the invisible character as <U+3164>, because the real character would print as nothing on this page.

var adm<U+3164>in = 1;                // a second identifier that reads as `admin`
var greeting = "hello<U+3164>world";  // a different string than the one you see

const { timeout, <U+3164> } = req.query; // a hidden parameter taken from the request…
exec(<U+3164>);                          // …and executed

const checkCommands = [
  'ping -c 1 google.com',
  'curl -s http://example.com/',<U+3164> // a hidden extra array element
];

// hidden<U+3164>comment

✅ Correct

// Plain identifiers and strings.
const admin = false;
const greeting = 'hello world';

// Korean and Khmer text is text.
const hello = '안녕하세요';

// A deliberate filler, written as an escape so review can see it.
const BLANK_NAME = '\u3164';

Options

This rule has no options. To ban further characters across the whole source, use no-bidi-characters' additionalCharacters option.

Error Message Format

🔒 CWE-1007 OWASP:A08-Software CVSS:5.3 | U+3164 HANGUL FILLER found in code. It renders as nothing but is a real character: inside a name it makes a different identifier, inside a string a different value, and alone it is an identifier a reviewer cannot see. | MEDIUM
   Fix: Remove the character. If a string genuinely needs it, write it as an escape (\u3164) so it is visible in review. | https://certitude.consulting/blog/en/invisible-backdoor/
🔒 CWE-1007 OWASP:A08-Software CVSS:5.3 | U+3164 HANGUL FILLER found in a comment. It renders as nothing, so the comment a reviewer reads is not the text that is there. | MEDIUM
   Fix: Remove the character. | https://certitude.consulting/blog/en/invisible-backdoor/

Known False Negatives

Characters outside the six

Why: The set is limited to invisible letters no sibling rule reports. Other blank or near-blank characters — U+2800 BRAILLE PATTERN BLANK, U+034F COMBINING GRAPHEME JOINER — have legitimate uses in text and are not in it.

Mitigation: Add them through no-bidi-characters' additionalCharacters, which scans the whole source the same way.

Invisible characters built at runtime

Why: String.fromCharCode(0x3164) or '\u3164' produce the character only when the code runs. The source a reviewer reads shows it plainly, which is the property this rule protects.

Mitigation: None needed for review; validate untrusted identifiers at runtime if your application accepts them.

When Not To Use It

A Korean-language codebase that uses U+3164 as a deliberate blank placeholder (for example, an empty-looking display name) will see those uses reported. Write them as the \u3164 escape instead of disabling the rule: the escape is visible, and the rule stays on for the place an attacker would put one.

Further Reading

Use this rule

npm install -D oxlint-plugin-secure-coding
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-secure-coding"],  "rules": {    "secure-coding/no-invisible-characters": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-secure-coding'],    rules: {      'secure-coding/no-invisible-characters': 'error',    },  },});

oxlint-plugin-secure-coding re-exports eslint-plugin-secure-coding/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.