Server hardening: finite numbers, open servers, unkickable host, no URL leaks

- Reject non-finite numerics at protocol parse and in the engine
  (a 1e400 seek became Infinity and broke every broadcast).
- Reject empty swaps; sanitize URL-looking media names.
- Blank password = open server; defaults no longer 'change-me'.
- Loopback peers (the in-process host) can't be kicked.
- /health no longer exposes media/transport; file URLs use the
  host the client dialled instead of 127.0.0.1.
- Count served bytes as streamed, not as requested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-07 00:30:12 +03:00
1 parent 32848aaf71
commit e2301aa4ec
12 files changed
+370 -45

No files matched your search

+24 -1
View File
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:sync_relay_core/core.dart';
import 'package:test/test.dart';
@@ -6,7 +8,8 @@ void main() {
test('defaults are sane', () {
final c = AppConfig.defaults();
expect(c.port, 8080);
expect(c.secret, isNotEmpty);
expect(c.secret, isEmpty, reason: 'empty password = open server');
expect(c.advertiseHost, isEmpty);
});
test('fromMap tolerates missing keys', () {
@@ -31,6 +34,26 @@ void main() {
});
group('protocol', () {
test('non-finite numbers are rejected', () {
final inf = jsonDecode('{"t":"seek","position":1e400}') as Map<String, Object?>;
expect(inf['position'], double.infinity);
expect(() => SeekIntent.fromJson(inf), throwsA(isA<ProtocolException>()));
expect(
() => ReportMessage.fromJson(
{'t': 'report', 'position': 1.0, 'buffered': double.infinity}),
throwsA(isA<ProtocolException>()));
expect(
() => ReportMessage.fromJson(
{'t': 'report', 'position': double.nan, 'buffered': 1.0}),
throwsA(isA<ProtocolException>()));
expect(
() => SwapIntent.fromJson({
't': 'swap',
'media': {'name': 'a', 'size': 1, 'duration': double.infinity}
}),
throwsA(isA<ProtocolException>()));
});
test('hello round-trips', () {
const msg = HelloMessage(
proto: 1,
+20
View File
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:sync_relay_core/core.dart';
import 'package:test/test.dart';
@@ -16,6 +18,24 @@ void main() {
void buffer(String token, double secs) =>
engine.applyReport(token, engine.positionAt(), secs, engine.eventId);
group('non-finite guards', () {
test('seek/report/swap reject Infinity and NaN, broadcast stays encodable', () {
join('a', 'alice');
engine.applySeek('a', double.infinity);
engine.applySeek('a', double.nan);
expect(engine.position, 0);
engine.applyReport('a', double.infinity, 1, engine.eventId);
engine.applyReport('a', 1, double.infinity, engine.eventId);
expect(engine.members['a']!.buffered, 0);
expect(
engine.applySwap('a',
const HelloMedia(name: 'x', size: 1, duration: double.infinity)),
isFalse);
expect(engine.mediaId, 1);
jsonEncode(engine.broadcast().toJson());
});
});
group('membership + gate', () {
test('join lands out-of-set, room stays held', () {
join('a', 'alice');