Refuse a non-finite number where a text field coerces one
Mirror / push (push) Successful in 4s
Test / lint (pull_request) Successful in 21s
Test / test (18) (pull_request) Successful in 36s
Test / test (20) (pull_request) Successful in 30s
Test / test (22) (pull_request) Successful in 30s
Test / test (24) (pull_request) Successful in 29s
Test / test (26) (pull_request) Successful in 30s

This commit is contained in:
2026-09-21 10:22:41 +02:00
parent e2edd7e31e
commit e7e71ca9bd
5 changed files with 20 additions and 7 deletions
+4
View File
@@ -25,6 +25,10 @@
will not match the sender it came from. Ids most SMSCs issue are digits or hex and are unaffected. will not match the sender it came from. Ids most SMSCs issue are digits or hex and are unaffected.
- A `U+0000` inside a C-Octet String — `source_addr`, `message_id`, `system_id` and the rest — is - A `U+0000` inside a C-Octet String — `source_addr`, `message_id`, `system_id` and the rest — is
refused. An Octet String carries a NULL as before. refused. An Octet String carries a NULL as before.
- A number with no decimal spelling — `NaN`, `Infinity`, `-Infinity` — is refused where a text field
takes one. `sendSms({ from: NaN })` put the literal sender `NaN` on the wire and reported the send
as successful; `message_id`, `source_addr` and every other text field took such a number the same
way. A finite number still writes its decimals, so `message_id: 123` is unchanged.
## 0.5.0 ## 0.5.0
+4
View File
@@ -99,6 +99,10 @@ function wantText(value: ParamValue): Result<{ text: string }> {
return { err: new Error(`Expected a string or a number, got ${typeof value}`) }; return { err: new Error(`Expected a string or a number, got ${typeof value}`) };
} }
if (typeof value === 'number' && !Number.isFinite(value)) {
return { err: new Error(`Expected a finite number, got ${String(value)}`) };
}
const text = String(value); const text = String(value);
return pastLatin1(text) ?? { text }; return pastLatin1(text) ?? { text };
+4
View File
@@ -153,6 +153,10 @@ describe('parsing real PDUs', () => {
assert.ok(smuggled.err instanceof Error); assert.ok(smuggled.err instanceof Error);
assert.equal(smuggled.buffer, undefined); assert.equal(smuggled.buffer, undefined);
assert.ok(objToPdu({ cmdName: 'deliver_sm', params: { source_addr: '一' } }).err instanceof Error); assert.ok(objToPdu({ cmdName: 'deliver_sm', params: { source_addr: '一' } }).err instanceof Error);
// String(NaN) is a sender the peer reads as the three letters, and the send reports success.
assert.ok(objToPdu({ cmdName: 'deliver_sm', params: { source_addr: NaN } }).err instanceof Error);
assert.ok(objToPdu({ cmdName: 'deliver_sm', params: { source_addr: Infinity } }).err instanceof Error);
}); });
}); });
+8 -1
View File
@@ -112,13 +112,20 @@ describe('cstring (C-Octet String)', () => {
assert.deepEqual(target, encoded); assert.deepEqual(target, encoded);
}); });
test('coerces a numeric value to its decimal string', () => { test('coerces a numeric value to its decimal string, and refuses one with no decimals', () => {
const target = Buffer.alloc(4); const target = Buffer.alloc(4);
types.cstring.write(123, target, 0); types.cstring.write(123, target, 0);
assert.deepEqual(target, Buffer.from([0x31, 0x32, 0x33, 0x00])); assert.deepEqual(target, Buffer.from([0x31, 0x32, 0x33, 0x00]));
assert.deepEqual(types.cstring.size(123), { size: 4 }); assert.deepEqual(types.cstring.size(123), { size: 4 });
for (const value of [NaN, Infinity, -Infinity]) {
assert.ok(types.cstring.size(value).err instanceof Error, String(value));
assert.ok(types.cstring.write(value, Buffer.alloc(9), 0).err instanceof Error, String(value));
assert.ok(types.string.write(value, Buffer.alloc(9), 0).err instanceof Error, String(value));
assert.ok(types.tlv.string.write(value, Buffer.alloc(9), 0).err instanceof Error, String(value));
}
}); });
test('carries every latin1 octet, and refuses a character past it', () => { test('carries every latin1 octet, and refuses a character past it', () => {
-6
View File
@@ -189,12 +189,6 @@ and is also what the panel ranked hardest — two methods, one answer.
### Correctness, ahead of everything below ### Correctness, ahead of everything below
- [ ] **Refuse a non-finite number where a text field coerces one.** `wantText()` in `defs/types.ts`
stringifies a number so `message_id: 123` writes `"123"`, which is deliberate and tested. It
takes `NaN` and `Infinity` on the same path, so `sendSms({ from: NaN })` puts the literal sender
`NaN` on the wire and reports success — goal 2. Gate the numeric branch on `Number.isFinite`.
Predates the latin1 guard; found by the stability review of #16.
- [ ] **Answer `alert_notification` and `outbind` by not answering them.** Both are response-less in - [ ] **Answer `alert_notification` and `outbind` by not answering them.** Both are response-less in
SMPP 3.4, both fall through `route()`'s default into `unhandled()`, which calls SMPP 3.4, both fall through `route()`'s default into `unhandled()`, which calls
`sendReturn(pduObj, 'ESME_RINVCMDID')`; `pduReturn()` then finds no response command, and the `sendReturn(pduObj, 'ESME_RINVCMDID')`; `pduReturn()` then finds no response command, and the