From 5ffb1227e6ae87dfc83fafc06278e0628adf522f Mon Sep 17 00:00:00 2001 From: sahvx655-wq Date: Sun, 31 May 2026 21:46:17 +0530 Subject: [PATCH] reject non-ASCII Unicode digits in check-digit routines --- .../routines/checkdigit/CUSIPCheckDigit.java | 2 +- .../routines/checkdigit/ISINCheckDigit.java | 5 +- .../checkdigit/ModulusCheckDigit.java | 4 +- .../checkdigit/ModulusTenCheckDigit.java | 2 +- .../routines/checkdigit/SedolCheckDigit.java | 2 +- .../checkdigit/VerhoeffCheckDigit.java | 7 +- .../CheckDigitNonAsciiDigitTest.java | 98 +++++++++++++++++++ 7 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 src/test/java/org/apache/commons/validator/routines/checkdigit/CheckDigitNonAsciiDigitTest.java diff --git a/src/main/java/org/apache/commons/validator/routines/checkdigit/CUSIPCheckDigit.java b/src/main/java/org/apache/commons/validator/routines/checkdigit/CUSIPCheckDigit.java index fe133ddcf..4eb55fdeb 100644 --- a/src/main/java/org/apache/commons/validator/routines/checkdigit/CUSIPCheckDigit.java +++ b/src/main/java/org/apache/commons/validator/routines/checkdigit/CUSIPCheckDigit.java @@ -69,7 +69,7 @@ protected int toInt(final char character, final int leftPos, final int rightPos) final int charValue = Character.getNumericValue(character); // the final character is only allowed to reach 9 final int charMax = rightPos == 1 ? 9 : 35; // CHECKSTYLE IGNORE MagicNumber - if (charValue < 0 || charValue > charMax) { + if (character > 0x7F || charValue < 0 || charValue > charMax) { throw new CheckDigitException("Invalid Character[" + leftPos + "," + rightPos + "] = '" + charValue + "' out of range 0 to " + charMax); } diff --git a/src/main/java/org/apache/commons/validator/routines/checkdigit/ISINCheckDigit.java b/src/main/java/org/apache/commons/validator/routines/checkdigit/ISINCheckDigit.java index 08d5cdb39..31794ea51 100644 --- a/src/main/java/org/apache/commons/validator/routines/checkdigit/ISINCheckDigit.java +++ b/src/main/java/org/apache/commons/validator/routines/checkdigit/ISINCheckDigit.java @@ -75,8 +75,9 @@ protected int calculateModulus(final String code, final boolean includesCheckDig } } for (int i = 0; i < code.length(); i++) { - final int charValue = Character.getNumericValue(code.charAt(i)); - if (charValue < 0 || charValue > MAX_ALPHANUMERIC_VALUE) { + final char character = code.charAt(i); + final int charValue = Character.getNumericValue(character); + if (character > 0x7F || charValue < 0 || charValue > MAX_ALPHANUMERIC_VALUE) { throw new CheckDigitException("Invalid Character[" + (i + 1) + "] = '" + charValue + "'"); } // this converts alphanumerics to two digits diff --git a/src/main/java/org/apache/commons/validator/routines/checkdigit/ModulusCheckDigit.java b/src/main/java/org/apache/commons/validator/routines/checkdigit/ModulusCheckDigit.java index dd65b670b..905e72644 100644 --- a/src/main/java/org/apache/commons/validator/routines/checkdigit/ModulusCheckDigit.java +++ b/src/main/java/org/apache/commons/validator/routines/checkdigit/ModulusCheckDigit.java @@ -179,8 +179,8 @@ protected String toCheckDigit(final int charValue) throws CheckDigitException { * @throws CheckDigitException if character is non-numeric */ protected int toInt(final char character, final int leftPos, final int rightPos) throws CheckDigitException { - if (Character.isDigit(character)) { - return Character.getNumericValue(character); + if (character >= '0' && character <= '9') { + return character - '0'; } throw new CheckDigitException("Invalid Character[" + leftPos + "] = '" + character + "'"); } diff --git a/src/main/java/org/apache/commons/validator/routines/checkdigit/ModulusTenCheckDigit.java b/src/main/java/org/apache/commons/validator/routines/checkdigit/ModulusTenCheckDigit.java index a5cf99f4e..bbb6537c1 100644 --- a/src/main/java/org/apache/commons/validator/routines/checkdigit/ModulusTenCheckDigit.java +++ b/src/main/java/org/apache/commons/validator/routines/checkdigit/ModulusTenCheckDigit.java @@ -207,7 +207,7 @@ public boolean isValid(final String code) { @Override protected int toInt(final char character, final int leftPos, final int rightPos) throws CheckDigitException { final int num = Character.getNumericValue(character); - if (num < 0) { + if (num < 0 || character > 0x7F) { throw new CheckDigitException("Invalid Character[" + leftPos + "] = '" + character + "'"); } return num; diff --git a/src/main/java/org/apache/commons/validator/routines/checkdigit/SedolCheckDigit.java b/src/main/java/org/apache/commons/validator/routines/checkdigit/SedolCheckDigit.java index ec6cd5cab..bbc68caae 100644 --- a/src/main/java/org/apache/commons/validator/routines/checkdigit/SedolCheckDigit.java +++ b/src/main/java/org/apache/commons/validator/routines/checkdigit/SedolCheckDigit.java @@ -87,7 +87,7 @@ protected int toInt(final char character, final int leftPos, final int rightPos) final int charValue = Character.getNumericValue(character); // the check digit is only allowed to reach 9 final int charMax = rightPos == 1 ? 9 : MAX_ALPHANUMERIC_VALUE; // CHECKSTYLE IGNORE MagicNumber - if (charValue < 0 || charValue > charMax) { + if (character > 0x7F || charValue < 0 || charValue > charMax) { throw new CheckDigitException("Invalid Character[" + leftPos + "," + rightPos + "] = '" + charValue + "' out of range 0 to " + charMax); } return charValue; diff --git a/src/main/java/org/apache/commons/validator/routines/checkdigit/VerhoeffCheckDigit.java b/src/main/java/org/apache/commons/validator/routines/checkdigit/VerhoeffCheckDigit.java index 443927ca8..570c512d5 100644 --- a/src/main/java/org/apache/commons/validator/routines/checkdigit/VerhoeffCheckDigit.java +++ b/src/main/java/org/apache/commons/validator/routines/checkdigit/VerhoeffCheckDigit.java @@ -102,11 +102,12 @@ private int calculateChecksum(final String code, final boolean includesCheckDigi int checksum = 0; for (int i = 0; i < code.length(); i++) { final int idx = code.length() - (i + 1); - final int num = Character.getNumericValue(code.charAt(idx)); - if (num < 0 || num > 9) { // CHECKSTYLE IGNORE MagicNumber + final char character = code.charAt(idx); + if (character < '0' || character > '9') { // CHECKSTYLE IGNORE MagicNumber throw new CheckDigitException("Invalid Character[" + - i + "] = '" + (int) code.charAt(idx) + "'"); + i + "] = '" + (int) character + "'"); } + final int num = character - '0'; final int pos = includesCheckDigit ? i : i + 1; checksum = D_TABLE[checksum][P_TABLE[pos % 8][num]]; // CHECKSTYLE IGNORE MagicNumber } diff --git a/src/test/java/org/apache/commons/validator/routines/checkdigit/CheckDigitNonAsciiDigitTest.java b/src/test/java/org/apache/commons/validator/routines/checkdigit/CheckDigitNonAsciiDigitTest.java new file mode 100644 index 000000000..f10f828b1 --- /dev/null +++ b/src/test/java/org/apache/commons/validator/routines/checkdigit/CheckDigitNonAsciiDigitTest.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.validator.routines.checkdigit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Tests that check digit routines reject non-ASCII Unicode digits that + * {@code Character.getNumericValue} / {@code Character.isDigit} otherwise map to 0-9. + */ +class CheckDigitNonAsciiDigitTest { + + private static String toFullwidth(final String code) { + final StringBuilder sb = new StringBuilder(code.length()); + for (int i = 0; i < code.length(); i++) { + final char c = code.charAt(i); + sb.append(c >= '0' && c <= '9' ? (char) ('0' + (c - '0')) : c); + } + return sb.toString(); + } + + private static String toArabicIndic(final String code) { + final StringBuilder sb = new StringBuilder(code.length()); + for (int i = 0; i < code.length(); i++) { + final char c = code.charAt(i); + sb.append(c >= '0' && c <= '9' ? (char) ('٠' + (c - '0')) : c); + } + return sb.toString(); + } + + private static void assertRejectsNonAscii(final CheckDigit routine, final String validCode) { + assertTrue(routine.isValid(validCode), "ASCII: " + validCode); + assertFalse(routine.isValid(toFullwidth(validCode)), "fullwidth: " + validCode); + assertFalse(routine.isValid(toArabicIndic(validCode)), "arabic-indic: " + validCode); + } + + @Test + void testCUSIP() { + assertRejectsNonAscii(CUSIPCheckDigit.CUSIP_CHECK_DIGIT, "037833100"); + } + + @Test + void testEAN13() { + assertRejectsNonAscii(EAN13CheckDigit.EAN13_CHECK_DIGIT, "9780072129519"); + } + + @Test + void testISIN() { + assertRejectsNonAscii(ISINCheckDigit.ISIN_CHECK_DIGIT, "US0378331005"); + } + + @Test + void testISBN10() { + assertRejectsNonAscii(ISBN10CheckDigit.ISBN10_CHECK_DIGIT, "1930110995"); + } + + @Test + void testISSN() { + assertRejectsNonAscii(ISSNCheckDigit.ISSN_CHECK_DIGIT, "03178471"); + } + + @Test + void testLuhn() { + assertRejectsNonAscii(LuhnCheckDigit.LUHN_CHECK_DIGIT, "4417123456789113"); + } + + @Test + void testModulusTenLuhn() { + assertRejectsNonAscii(new ModulusTenCheckDigit(new int[] { 1, 2 }, true, true), "4417123456789113"); + } + + @Test + void testSedol() { + assertRejectsNonAscii(SedolCheckDigit.SEDOL_CHECK_DIGIT, "0263494"); + } + + @Test + void testVerhoeff() { + assertRejectsNonAscii(VerhoeffCheckDigit.VERHOEFF_CHECK_DIGIT, "1428570"); + } +}