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

+21 -6
View File
@@ -8,6 +8,15 @@ import 'package:sync_relay_server/server.dart';
import 'package:test/test.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
Future<String?> _lanAddress() async {
for (final i in await NetworkInterface.list(type: InternetAddressType.IPv4)) {
for (final a in i.addresses) {
if (!a.isLoopback) return a.address;
}
}
return null;
}
void main() {
group('room auth live', () {
late File file;
@@ -73,7 +82,7 @@ void main() {
expect(decoded['proto'], RelayConstants.protoVersion);
expect(decoded['viewers'], 0);
expect(decoded['bytesServed'], 0);
expect(decoded['media'], isNotNull);
expect(decoded.containsKey('media'), isFalse, reason: 'no media name leak');
});
test('wrong secret gets err, right secret gets welcome+token', () async {
@@ -238,10 +247,16 @@ void main() {
});
test('kick via socket: peer kicked, unknown target errors', () async {
WebSocketChannel open() => WebSocketChannel.connect(
Uri.parse('ws://127.0.0.1:${server.boundPort}/sync'));
Future<(WebSocketChannel, String, String, List<String>)> join(String who) async {
final ch = open();
final lan = await _lanAddress();
if (lan == null) {
markTestSkipped('no non-loopback IPv4 to act as a non-host peer');
return;
}
// Loopback peers are the host and unkickable, so bob dials the LAN address.
Future<(WebSocketChannel, String, String, List<String>)> join(String who,
[String host = '127.0.0.1']) async {
final ch = WebSocketChannel.connect(
Uri.parse('ws://$host:${server.boundPort}/sync'));
final messages = <String>[];
ch.stream.listen((m) => messages.add('$m'));
final msg = HelloMessage(
@@ -289,7 +304,7 @@ void main() {
}
final (a, _, _, messagesA) = await join('alice');
final (b, tokenB, idB, messagesB) = await join('bob');
final (b, tokenB, idB, messagesB) = await join('bob', lan);
try {
a.sink.add(jsonEncode(
KickRequest(target: idB, reason: 'afk?').toJson()));