fix: catch punctuation-separated slurs in username censor

Re-add skipNonAlphabeticTransformer to both matcher chains so bypass
attempts like "n.i.g.g.e.r" are detected.
This commit is contained in:
evanpelle
2026-05-04 18:18:36 -06:00
parent 08b9fd96e6
commit ffbe48ad10
2 changed files with 24 additions and 6 deletions
+8 -1
View File
@@ -6,6 +6,7 @@ import {
pattern, pattern,
resolveConfusablesTransformer, resolveConfusablesTransformer,
resolveLeetSpeakTransformer, resolveLeetSpeakTransformer,
skipNonAlphabeticTransformer,
toAsciiLowerCaseTransformer, toAsciiLowerCaseTransformer,
} from "obscenity"; } from "obscenity";
import countries from "resources/countries.json"; import countries from "resources/countries.json";
@@ -71,15 +72,21 @@ export function createMatcher(bannedWords: string[]): RegExpMatcher {
]; ];
// substringMatcher: literal patterns, no collapse — catches "niggertesting" as a substring // substringMatcher: literal patterns, no collapse — catches "niggertesting" as a substring
// collapseMatcher: deduped patterns + collapse transformer — catches "niiiigger", "hiiitler" // collapseMatcher: deduped patterns + collapse transformer — catches "niiiigger", "hiiitler"
// skipNonAlphabeticTransformer is applied last to catch punctuation-separated bypasses
// like "n.i.g.g.e.r".
const substringMatcher = new RegExpMatcher({ const substringMatcher = new RegExpMatcher({
...buildDataset(bannedWords, false), ...buildDataset(bannedWords, false),
blacklistMatcherTransformers: baseTransformers, blacklistMatcherTransformers: [
...baseTransformers,
skipNonAlphabeticTransformer(),
],
}); });
const collapseMatcher = new RegExpMatcher({ const collapseMatcher = new RegExpMatcher({
...buildDataset(bannedWords, true), ...buildDataset(bannedWords, true),
blacklistMatcherTransformers: [ blacklistMatcherTransformers: [
...baseTransformers, ...baseTransformers,
collapseDuplicatesTransformer(), collapseDuplicatesTransformer(),
skipNonAlphabeticTransformer(),
], ],
}); });
return { return {
+16 -5
View File
@@ -114,11 +114,9 @@ describe("UsernameCensor", () => {
expect(matcher.hasMatch("MyChairName")).toBe(true); expect(matcher.hasMatch("MyChairName")).toBe(true);
}); });
test("detects banned words with underscores/dots/numbers mixed in", () => { test("detects banned words with non-alphabetic characters mixed in", () => {
// These should NOT bypass the filter (skipNonAlphabetic was intentionally removed) expect(matcher.hasMatch("n.i.g.g.e.r")).toBe(true);
// Words separated by non-alpha chars are treated as separate tokens expect(matcher.hasMatch("hi_tler")).toBe(true);
expect(matcher.hasMatch("n.i.g.g.e.r")).toBe(false); // dots break the word
expect(matcher.hasMatch("hi_tler")).toBe(false); // underscore breaks it
}); });
test("allows clean usernames", () => { test("allows clean usernames", () => {
@@ -141,6 +139,19 @@ describe("UsernameCensor", () => {
expect(matcher.hasMatch("kkklover")).toBe(true); expect(matcher.hasMatch("kkklover")).toBe(true);
expect(matcher.hasMatch("ilovekkkboys")).toBe(true); expect(matcher.hasMatch("ilovekkkboys")).toBe(true);
}); });
test("catches slurs separated by periods (bypass attempt)", () => {
expect(matcher.hasMatch("n.i.g.g.e.r")).toBe(true);
expect(matcher.hasMatch("N.I.G.G.E.R")).toBe(true);
expect(matcher.hasMatch("n.i.g.g.a")).toBe(true);
expect(matcher.hasMatch("h.i.t.l.e.r")).toBe(true);
expect(matcher.hasMatch("hello n.i.g.g.e.r world")).toBe(true);
});
test("censor replaces period-separated slur usernames", () => {
const result = checker.censor("n.i.g.g.e.r", null);
expect(shadowNames).toContain(result.username);
});
}); });
describe("censor", () => { describe("censor", () => {