fix: filter garbage channel names from /api/channels, fixes #201

Channels with garbage-decrypted names (pre-#197 data still in DB) are now
filtered at the API level using the same non-printable character heuristic
from #197. Applied in both Node.js server.js and Go server (store.go, db.go).
No data is deleted — only filtered from API responses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kpa-clawbot
2026-03-27 22:49:45 -07:00
parent 848ddf7fb7
commit 9ebfd40aa0
4 changed files with 64 additions and 1 deletions
+6
View File
@@ -1218,6 +1218,12 @@ func (db *DB) GetChannels() ([]map[string]interface{}, error) {
if dtype != "CHAN" {
continue
}
// Filter out garbage-decrypted channel names/messages (pre-#197 data still in DB)
chanStr, _ := decoded["channel"].(string)
textStr, _ := decoded["text"].(string)
if hasGarbageChars(chanStr) || hasGarbageChars(textStr) {
continue
}
channelName, _ := decoded["channel"].(string)
if channelName == "" {
channelName = "unknown"
+24
View File
@@ -12,6 +12,7 @@ import (
"sync"
"sync/atomic"
"time"
"unicode/utf8"
)
// payloadTypeNames maps payload_type int → human-readable name (firmware-standard).
@@ -1538,6 +1539,25 @@ func filterTxSlice(s []*StoreTx, fn func(*StoreTx) bool) []*StoreTx {
return result
}
// countNonPrintable counts characters that are non-printable (< 0x20 except \n, \t)
// or invalid UTF-8 replacement characters. Mirrors the heuristic from #197.
func countNonPrintable(s string) int {
count := 0
for _, r := range s {
if r < 0x20 && r != '\n' && r != '\t' {
count++
} else if r == utf8.RuneError {
count++
}
}
return count
}
// hasGarbageChars returns true if the string contains garbage (non-printable) data.
func hasGarbageChars(s string) bool {
return s != "" && (!utf8.ValidString(s) || countNonPrintable(s) > 2)
}
// GetChannels returns channel list from in-memory packets (payload_type 5, decoded type CHAN).
func (s *PacketStore) GetChannels(region string) []map[string]interface{} {
s.mu.RLock()
@@ -1588,6 +1608,10 @@ func (s *PacketStore) GetChannels(region string) []map[string]interface{} {
if decoded.Type != "CHAN" {
continue
}
// Filter out garbage-decrypted channel names/messages (pre-#197 data still in DB)
if hasGarbageChars(decoded.Channel) || hasGarbageChars(decoded.Text) {
continue
}
channelName := decoded.Channel
if channelName == "" {
+1 -1
View File
@@ -360,7 +360,7 @@ function validateAdvert(advert) {
return { valid: true };
}
module.exports = { decodePacket, validateAdvert, ROUTE_TYPES, PAYLOAD_TYPES, VALID_ROLES };
module.exports = { decodePacket, validateAdvert, hasNonPrintableChars, ROUTE_TYPES, PAYLOAD_TYPES, VALID_ROLES };
// --- Tests ---
if (require.main === module) {
+33
View File
@@ -121,6 +121,28 @@ function seedTestData() {
try { pktStore.insert(chanPkt2); } catch {}
try { db.insertTransmission(chanPkt2); } catch {}
// Seed a CHAN packet with garbage-decrypted name (pre-#197 data) — should be filtered out
const garbageChanPkt = {
raw_hex: 'GARBAGE0CHANNEL1',
timestamp: now, observer_id: 'test-obs-1', observer_name: 'TestObs', snr: 2, rssi: -95,
hash: 'test-hash-garbage-chan', route_type: 0, payload_type: 5, payload_version: 1,
path_json: JSON.stringify([]),
decoded_json: JSON.stringify({ type: 'CHAN', channel: 'garb\x01\x02\x03age', text: 'SomeUser: hello\x04\x05\x06', sender: 'SomeUser' }),
};
try { pktStore.insert(garbageChanPkt); } catch {}
try { db.insertTransmission(garbageChanPkt); } catch {}
// Seed a CHAN packet with clean name but garbage text — should also be filtered out
const garbageTextPkt = {
raw_hex: 'GARBAGE0TEXT0001',
timestamp: now, observer_id: 'test-obs-1', observer_name: 'TestObs', snr: 2, rssi: -95,
hash: 'test-hash-garbage-text', route_type: 0, payload_type: 5, payload_version: 1,
path_json: JSON.stringify([]),
decoded_json: JSON.stringify({ type: 'CHAN', channel: 'cleanChan', text: '\x00\x01\x02\x03garbage binary', sender: 'User' }),
};
try { pktStore.insert(garbageTextPkt); } catch {}
try { db.insertTransmission(garbageTextPkt); } catch {}
// Packet with sender_key/recipient_key for peer interaction coverage in db.getNodeAnalytics
const peerPkt = {
raw_hex: 'DEADBEEF00112233',
@@ -436,6 +458,17 @@ seedTestData();
assert(typeof r.body === 'object', 'should return channels');
});
await t('GET /api/channels filters garbage channel names', async () => {
cache.clear();
const r = await request(app).get('/api/channels').expect(200);
const names = r.body.channels.map(c => c.name);
assert(!names.some(n => n.includes('\x01') || n.includes('\x02') || n.includes('\x03')),
'garbage channel names should be filtered out');
assert(!names.includes('cleanChan'),
'channels with garbage text should be filtered out');
assert(names.includes('ch01'), 'valid channel ch01 should still be present');
});
await t('GET /api/channels with region', async () => {
await request(app).get('/api/channels?region=SFO').expect(200);
});