From 15b5d28325b4fa22902c04de46086c3470cf3e2e Mon Sep 17 00:00:00 2001 From: uwezkhan <114483941+uwezkhan@users.noreply.github.com> Date: Sun, 28 Jun 2026 16:48:20 +0530 Subject: [PATCH] Merge pull request #29378 from uwezkhan:qr-alpha-map-bound bound alphanumeric values in qr decodeAlpha before map lookup #29378 decodeAlpha in the no-quirc QR backend reads alphanumeric symbols from the post-ECC bitstream and indexes a fixed 45-entry map[] with the raw values. The 11-bit pair from next(11) goes up to 2047, so tuple/45 lands on 45 once the pair passes 2024, and the 6-bit trailing char from next(6) goes up to 63, so map[value] runs out to map[63]. A QR whose data codewords carry an alphanumeric segment with one of those out-of-range values reads past the static array, and the stray byte lands in the string returned by detectAndDecode. WITH_QUIRC defaults off, so this is the decoder a stock build runs. Before, the only guard was on the encode side, which never emits those values, so the decoder trusted the stream and indexed map[] directly. New version checks value range and return empty string for malformed qr codes. ### Pull Request Readiness Checklist - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable - [ ] The feature is well documented and sample code can be built with the project CMake --- modules/objdetect/src/qrcode_encoder.cpp | 40 +++++++++++++------- modules/objdetect/test/test_qrcode.cpp | 48 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/modules/objdetect/src/qrcode_encoder.cpp b/modules/objdetect/src/qrcode_encoder.cpp index a58eb08f9e..a08951ad77 100644 --- a/modules/objdetect/src/qrcode_encoder.cpp +++ b/modules/objdetect/src/qrcode_encoder.cpp @@ -1359,11 +1359,11 @@ private: void extractCodewords(Mat& source, std::vector& codewords); bool errorCorrection(std::vector& codewords); bool errorCorrectionBlock(std::vector& codewords); - void decodeSymbols(String& result); + bool decodeSymbols(String& result); void decodeNumeric(String& result); - void decodeAlpha(String& result); + bool decodeAlpha(String& result); void decodeByte(String& result); - void decodeECI(String& result); + bool decodeECI(String& result); void decodeKanji(String& result); void decodeStructuredAppend(String& result); }; @@ -1472,7 +1472,10 @@ bool QRCodeDecoderImpl::run(const Mat& straight, String& decoded_info) { if (!errorCorrection(bitstream.data)) { return false; } - decodeSymbols(decoded_info); + if (!decodeSymbols(decoded_info)) { + decoded_info = ""; + return false; + } return true; } @@ -1737,7 +1740,7 @@ void QRCodeDecoderImpl::extractCodewords(Mat& source, std::vector& code } } -void QRCodeDecoderImpl::decodeSymbols(String& result) { +bool QRCodeDecoderImpl::decodeSymbols(String& result) { CV_Assert(!bitstream.empty()); // Decode depends on the mode @@ -1750,15 +1753,19 @@ void QRCodeDecoderImpl::decodeSymbols(String& result) { } if (currMode == 0 || bitstream.empty()) - return; + return true; if (currMode == QRCodeEncoder::EncodeMode::MODE_NUMERIC) decodeNumeric(result); - else if (currMode == QRCodeEncoder::EncodeMode::MODE_ALPHANUMERIC) - decodeAlpha(result); + else if (currMode == QRCodeEncoder::EncodeMode::MODE_ALPHANUMERIC) { + if (!decodeAlpha(result)) + return false; + } else if (currMode == QRCodeEncoder::EncodeMode::MODE_BYTE) decodeByte(result); - else if (currMode == QRCodeEncoder::EncodeMode::MODE_ECI) - decodeECI(result); + else if (currMode == QRCodeEncoder::EncodeMode::MODE_ECI) { + if (!decodeECI(result)) + return false; + } else if (currMode == QRCodeEncoder::EncodeMode::MODE_KANJI) decodeKanji(result); else if (currMode == QRCodeEncoder::EncodeMode::MODE_STRUCTURED_APPEND) { @@ -1769,6 +1776,7 @@ void QRCodeDecoderImpl::decodeSymbols(String& result) { else CV_Error(Error::StsNotImplemented, format("mode %d", currMode)); } + return true; } void QRCodeDecoderImpl::decodeNumeric(String& result) { @@ -1788,7 +1796,7 @@ void QRCodeDecoderImpl::decodeNumeric(String& result) { } } -void QRCodeDecoderImpl::decodeAlpha(String& result) { +bool QRCodeDecoderImpl::decodeAlpha(String& result) { static const char map[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', @@ -1798,13 +1806,18 @@ void QRCodeDecoderImpl::decodeAlpha(String& result) { int num = bitstream.next(version <= 9 ? 9 : (version <= 26 ? 11 : 13)); for (int i = 0; i < num / 2; ++i) { int tuple = bitstream.next(11); + if (tuple >= 45 * 45) + return false; result += map[tuple / 45]; result += map[tuple % 45]; } if (num % 2) { int value = bitstream.next(6); + if (value >= 45) + return false; result += map[value]; } + return true; } void QRCodeDecoderImpl::decodeByte(String& result) { @@ -1814,7 +1827,7 @@ void QRCodeDecoderImpl::decodeByte(String& result) { } } -void QRCodeDecoderImpl::decodeECI(String& result) { +bool QRCodeDecoderImpl::decodeECI(String& result) { int eciAssignValue = bitstream.next(8); for (int i = 0; i < 8; ++i) { if (eciAssignValue & 1 << (7 - i)) @@ -1825,8 +1838,7 @@ void QRCodeDecoderImpl::decodeECI(String& result) { if (this->eci == 0) { this->eci = static_cast(eciAssignValue); } - decodeSymbols(result); - + return decodeSymbols(result); } void QRCodeDecoderImpl::decodeKanji(String& result) { diff --git a/modules/objdetect/test/test_qrcode.cpp b/modules/objdetect/test/test_qrcode.cpp index c49480d38f..f6dfb14aa1 100644 --- a/modules/objdetect/test/test_qrcode.cpp +++ b/modules/objdetect/test/test_qrcode.cpp @@ -571,6 +571,54 @@ TEST(Objdetect_QRCode_decode, decode_regression_version_25) EXPECT_EQ(expect_msg, decoded_msg); } +TEST(Objdetect_QRCode_decode, decode_alphanumeric_out_of_range) +{ + // Version 1 QR with a valid Reed-Solomon block whose alphanumeric segment + // carries an 11-bit pair value of 2047. decodeAlpha computes 2047 / 45 == 45 + // and used to read map[45], one past the 45-entry table, leaking an adjacent + // byte into the decoded string. The decoder must reject it and return an + // empty string instead. + static const char* modules[21] = { + "000000011011010000000", + "011111010011010111110", + "010001011011010100010", + "010001010011010100010", + "010001011011010100010", + "011111010011010111110", + "000000010101010000000", + "111111111011011111111", + "000001000011001010101", + "111111101001011011000", + "000111010001011011000", + "001001100111011011000", + "000001011011011011000", + "111111110101011011000", + "000000010111011011001", + "011111011101011011001", + "010001010001011011011", + "010001010001011011011", + "010001010001011011011", + "011111010001011011010", + "000000010001011011011" + }; + Mat qr(21, 21, CV_8UC1); + for (int i = 0; i < 21; i++) + for (int j = 0; j < 21; j++) + qr.at(i, j) = modules[i][j] == '1' ? 255 : 0; + + Mat src; + cv::resize(qr, src, qr.size() * 10, 0, 0, INTER_NEAREST); + cv::copyMakeBorder(src, src, 40, 40, 40, 40, BORDER_CONSTANT, Scalar(255)); + + QRCodeDetector qrcode; + std::vector corners; + ASSERT_TRUE(qrcode.detect(src, corners)); + Mat straight_barcode; + std::string decoded_info; + EXPECT_NO_THROW(decoded_info = qrcode.decode(src, corners, straight_barcode)); + EXPECT_TRUE(decoded_info.empty()); +} + TEST_P(Objdetect_QRCode_detectAndDecodeMulti, decode_9_qrcodes_version7) { const std::string name_current_image = "9_qrcodes_version7.jpg";