Address the CodeRabbit review: inbound SMS, binary payloads and untrusted input
This commit is contained in:
+27
-11
@@ -87,7 +87,10 @@ const latin1: Encoding = {
|
||||
|
||||
const ucs2: Encoding = {
|
||||
decode(buffer) {
|
||||
return Buffer.from(buffer).swap16().toString('utf16le');
|
||||
// A peer-controlled sm_length can cut a character in half, and swap16() refuses odd lengths.
|
||||
const whole = buffer.length - (buffer.length % 2);
|
||||
|
||||
return Buffer.from(buffer.subarray(0, whole)).swap16().toString('utf16le');
|
||||
},
|
||||
|
||||
encode(value) {
|
||||
@@ -113,22 +116,35 @@ export function detect(value: string): EncodingName {
|
||||
return 'UCS2';
|
||||
}
|
||||
|
||||
/**
|
||||
* SMPP data_coding is a flat table for 0x00-0x0E, but the 0x1X and 0xFX ranges carry a GSM message
|
||||
* class and encode the alphabet in bits 3-2 (or bit 2) instead — which is how a flash UCS2 message
|
||||
* arrives as 0x18. Alphabets with no codec here fall back to ASCII.
|
||||
*/
|
||||
export function encodingByDataCoding(dataCoding: number): EncodingName {
|
||||
/** The 0x1X and 0xFX ranges carry a GSM message class and put the alphabet in bits 3-2 or bit 2. */
|
||||
function messageClassEncoding(dataCoding: number): EncodingName | undefined {
|
||||
if ((dataCoding & 0xF0) === 0x10) {
|
||||
return ((dataCoding >> 2) & 0x03) === 0x02 ? 'UCS2' : 'ASCII';
|
||||
const alphabet = (dataCoding >> 2) & 0x03;
|
||||
|
||||
if (alphabet === 0x01) return 'LATIN1';
|
||||
|
||||
return alphabet === 0x02 ? 'UCS2' : 'ASCII';
|
||||
}
|
||||
|
||||
if ((dataCoding & 0xF0) === 0xF0) {
|
||||
return 'ASCII';
|
||||
return (dataCoding & 0x04) === 0x04 ? 'LATIN1' : 'ASCII';
|
||||
}
|
||||
|
||||
if (dataCoding === 0x03) return 'LATIN1';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* SMPP data_coding is a flat table for 0x00-0x0E, and the message class ranges are how a flash UCS2
|
||||
* message arrives as 0x18. The 8-bit binary codings resolve to LATIN1, the one codec here that maps
|
||||
* every octet to a code point and back unchanged, so a binary payload survives; alphabets with no
|
||||
* codec fall back to ASCII.
|
||||
*/
|
||||
export function encodingByDataCoding(dataCoding: number): EncodingName {
|
||||
const messageClass = messageClassEncoding(dataCoding);
|
||||
|
||||
if (messageClass) return messageClass;
|
||||
if (dataCoding === 0x08) return 'UCS2';
|
||||
|
||||
return 'ASCII';
|
||||
// 0x02 and 0x04 are 8-bit binary, 0x03 is Latin-1.
|
||||
return dataCoding >= 0x02 && dataCoding <= 0x04 ? 'LATIN1' : 'ASCII';
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ const specs = tlvSpecs({
|
||||
source_addr_subunit: { id: 0x000D, tag: 'source_addr_subunit', type: tlv.int8 },
|
||||
source_network_type: { id: 0x000E, tag: 'source_network_type', type: tlv.int8 },
|
||||
source_bearer_type: { id: 0x000F, tag: 'source_bearer_type', type: tlv.int8 },
|
||||
source_telematics_id: { id: 0x0010, tag: 'source_telematics_id', type: tlv.int16 },
|
||||
source_telematics_id: { id: 0x0010, tag: 'source_telematics_id', type: tlv.int8 },
|
||||
qos_time_to_live: { id: 0x0017, tag: 'qos_time_to_live', type: tlv.int32 },
|
||||
payload_type: { id: 0x0019, tag: 'payload_type', type: tlv.int8 },
|
||||
additional_status_info_text: { id: 0x001D, tag: 'additional_status_info_text', type: tlv.cstring },
|
||||
|
||||
+35
-3
@@ -142,6 +142,15 @@ function wantUnsuccessSmes(value: ParamValue): Result<{ smes: UnsuccessSme[] }>
|
||||
}
|
||||
|
||||
function readCstring(buffer: Buffer, offset: number): Result<{ bytesRead: number; value: string }> {
|
||||
// An offset at the end exactly is an absent trailing field, which real peers do send.
|
||||
if (outOfRange(buffer, offset, 0)) {
|
||||
return {
|
||||
err: new Error(
|
||||
`C-Octet String starts at offset ${String(offset)}, past a ${String(buffer.length)} octet buffer`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
|
||||
while (buffer[offset + length]) {
|
||||
@@ -197,6 +206,29 @@ export const int8 = intType(1, 0xFF, (b, o) => b.readUInt8(o), (b, v, o) => b.wr
|
||||
export const int16 = intType(2, 0xFFFF, (b, o) => b.readUInt16BE(o), (b, v, o) => b.writeUInt16BE(v, o));
|
||||
export const int32 = intType(4, 0xFFFFFFFF, (b, o) => b.readUInt32BE(o), (b, v, o) => b.writeUInt32BE(v, o));
|
||||
|
||||
const intByOctets: Record<number, WireType<number>> = { 1: int8, 2: int16, 4: int32 };
|
||||
|
||||
/**
|
||||
* The TLV header's length is what the parser skips past, so it is also the width the value is read
|
||||
* at — a peer that types a tag one octet wider than the table says still gets the value it meant.
|
||||
*/
|
||||
function tlvInt(declared: WireType<number>): WireType<number> {
|
||||
return {
|
||||
...declared,
|
||||
read(buffer, offset, length) {
|
||||
if (length === undefined) return declared.read(buffer, offset);
|
||||
|
||||
const width = intByOctets[length];
|
||||
|
||||
if (!width) {
|
||||
return { err: new Error(`Integer TLV declares ${String(length)} octets, expected 1, 2 or 4`) };
|
||||
}
|
||||
|
||||
return width.read(buffer, offset);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Octet String: a length octet followed by that many octets. */
|
||||
export const string: WireType<string> = {
|
||||
default: '',
|
||||
@@ -513,9 +545,9 @@ export const tlv = {
|
||||
return err ? { err } : writeCstring(text, buf, offset);
|
||||
},
|
||||
} satisfies WireType<string>,
|
||||
int8,
|
||||
int16,
|
||||
int32,
|
||||
int8: tlvInt(int8),
|
||||
int16: tlvInt(int16),
|
||||
int32: tlvInt(int32),
|
||||
string: {
|
||||
default: '',
|
||||
read(buf: Buffer, offset: number, length = 0) {
|
||||
|
||||
Reference in New Issue
Block a user