Skip to content

[BUG] proto_smpp: buffer overflow in recv_smpp_msg via unchecked sm_length #3846

@jming912

Description

@jming912

OpenSIPS version you are running

Latest master (commit a74fc46, 2026-03-19)

Describe the bug

recv_smpp_msg() in modules/proto_smpp/smpp.c uses a fixed-size static buffer sms_body[280] (i.e. 2 * MAX_SMS_CHARACTERS) to hold the converted message body. However, the sm_length field from the incoming SMPP PDU (uint8_t, range 0–255) is passed directly to convert_gsm7_to_utf8() and string2hex() without any bounds checking.

Both conversion functions can produce output significantly larger than 280 bytes, causing a static buffer overflow write of up to 230 bytes.

Path 1: GSM7 default encoding (data_coding != UCS2)

// smpp.c, recv_smpp_msg()
static char sms_body[2*MAX_SMS_CHARACTERS];  // 280 bytes
body_str.len = convert_gsm7_to_utf8((unsigned char *)body->short_message,
        body->sm_length, sms_body);

Many GSM7 characters (e.g. index 0x01 = £, 0x03 = ¥, 0x04 = è) map to Unicode codepoints > 0x7F, requiring 2 bytes of UTF-8 output per input byte. With sm_length = 255: output up to 510 bytes into a 280-byte buffer → overflow 230 bytes.

Path 2: UCS-2 encoding (data_coding == 8)

body_str.len = string2hex((char *)body->short_message,
        body->sm_length, sms_body);

string2hex() produces exactly 2 output bytes per input byte. With sm_length = 255: output 510 bytes → same 230-byte overflow.

Root cause: sm_length is read from the PDU without validation:

body->sm_length = *p++;
copy_fixed_str(body->short_message, p, body->sm_length);

No check that sm_length <= MAX_SMS_CHARACTERS before the conversion functions.

Note: short_message is char[254], so sm_length = 255 also causes a 1-byte overflow in copy_fixed_str.

To Reproduce

Compile the following standalone PoC with AddressSanitizer:

// Compile: clang -fsanitize=address -g -o poc_gsm7 poc_gsm7.c
#include <stdio.h>
#include <string.h>
#include <stdint.h>

#define MAX_SMS_CHARACTERS 140

static unsigned int table_gsm7_to_utf8[] = {
    '@',  0xA3,   '$',  0xA5,  0xE8,  0xE9,  0xF9,  0xEC,
   0xF2,  0xC7,  0x10,  0xd8,  0xF8,  0x13,  0xC5,  0xE5,
  0x394,   '_', 0x3A6, 0x393, 0x39B, 0x3A9, 0x3A0, 0x3A8,
  0x3A3, 0x398, 0x39E,   '?',  0xC6,  0xE6,  0xDF,  0xC9,
    ' ',   '!',   '"',   '#',  0xA4,   '%',   '&',  '\'',
    '(',   ')',   '*',   '+',   ',',   '-',   '.',   '/',
    '0',   '1',   '2',   '3',   '4',   '5',   '6',   '7',
    '8',   '9',   ':',   ';',   '<',   '=',   '>',   '?',
  0xA1,   'A',   'B',   'C',   'D',   'E',   'F',   'G',
    'H',   'I',   'J',   'K',   'L',   'M',   'N',   'O',
    'P',   'Q',   'R',   'S',   'T',   'U',   'V',   'W',
    'X',   'Y',   'Z',  0xC4,  0xD6,  0xD1,  0xDC,  0xA7,
   0xBF,   'a',   'b',   'c',   'd',   'e',   'f',   'g',
    'h',   'i',   'j',   'k',   'l',   'm',   'n',   'o',
    'p',   'q',   'r',   's',   't',   'u',   'v',   'w',
    'x',   'y',   'z',  0xE4,  0xF6,  0xF1,  0xFC,  0xE0,
};

static int convert_gsm7_to_utf8(unsigned char *input, int input_len, char *output) {
    char *p = output;
    int i, t;
    unsigned char c;
    for (i = 0; i < input_len; i++) {
        c = input[i];
        if (c == 0x1B) { switch (input[++i]) {
            case 0x14: t = '^'; break;  case 0x28: t = '{'; break;
            case 0x29: t = '}'; break;  case 0x2F: t = '\\'; break;
            case 0x3C: t = '['; break;  case 0x3D: t = '~'; break;
            case 0x3E: t = ']'; break;  case 0x40: t = '|'; break;
            case 0x65: t = 0x20AC; break;
            default: --i; case 0x0D: case 0x1B: t = '?'; break;
        }} else if (c < 0x80) t = table_gsm7_to_utf8[c]; else t = c;
        if (t > 0x7F) {
            if (t > 0x800) { *p++ = 0xE0|((t>>12)&0x0F); *p++ = 0x80|((t>>6)&0x3F); *p++ = 0x80|(t&0x3F); }
            else { *p++ = 0xC0|((t>>6)&0x1F); *p++ = 0x80|(t&0x3F); }
        } else *p++ = (unsigned char)t;
    }
    return p - output;
}

int main(void) {
    static char sms_body[2 * MAX_SMS_CHARACTERS]; // 280 bytes, same as OpenSIPS
    unsigned char input[200];
    memset(input, 0x01, sizeof(input)); // 0x01 = £ → 2-byte UTF-8 (0xC2 0xA3)
    convert_gsm7_to_utf8(input, 200, sms_body); // 400 bytes output → 120 bytes overflow
    return 0;
}

Run:

$ clang -fsanitize=address -g -o poc_gsm7 poc_gsm7.c && ./poc_gsm7
==ERROR: AddressSanitizer: global-buffer-overflow on address ...
WRITE of size 1 at ... thread T0
    #0 in convert_gsm7_to_utf8
    #1 in main
... is located 0 bytes after global variable 'sms_body' of size 280

Expected behavior

sm_length should be validated against MAX_SMS_CHARACTERS before being passed to the conversion functions. PDUs with sm_length > 140 (or > 254 for the short_message array) should be rejected.

Relevant System Logs

Without ASAN, this corrupts adjacent static/global variables silently. With ASAN enabled, the process aborts on the buffer overflow write.

OS/environment information

  • Operating System: Ubuntu 22.04
  • OpenSIPS installation: git (master, commit a74fc46)

Additional context

  • CWE: CWE-787 (Out-of-bounds Write)
  • Attack vector: Network — send a crafted deliver_sm or submit_sm SMPP PDU with sm_length > 140 and GSM7 characters that expand to multi-byte UTF-8 (or UCS-2 encoding path)
  • Authentication: The handle_smpp_msg() dispatch does not check session bind state, so this is reachable without authentication
  • Severity: High — static buffer overflow write of up to 230 bytes, corruption of adjacent global state, potential crash or code execution

Suggested fix — validate sm_length before conversion:

if (body->sm_length > MAX_SMS_CHARACTERS) {
    LM_ERR("sm_length %d exceeds maximum %d\n", body->sm_length, MAX_SMS_CHARACTERS);
    return -1;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions