Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Sources/NIOSSH/SSHMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,13 @@ extension ByteBuffer {
}
return .channelFailure(message)
default:
throw SSHMessage.ParsingError.unknownType(type)
// Unknown SSH message type - consume remaining bytes and return as ignore.
// This prevents parser state corruption when encountering extension messages
// (e.g. hostkeys-00@openssh.com) that NIOSSH doesn't support. Without this,
// the parser would throw, rewind the buffer reader over already-decrypted data,
// and skip incrementing the sequence number, causing cascading MAC failures.
self.moveReaderIndex(to: self.writerIndex)
return .ignore(.init(data: ByteBuffer()))
}
}
}
Expand Down
56 changes: 55 additions & 1 deletion Tests/NIOSSHTests/SSHMessagesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -748,8 +748,13 @@ final class SSHMessagesTests: XCTestCase {

XCTAssertNil(try buffer.readSSHMessage())

// Unknown message types should be ignored, not throw (per RFC 4253 Section 11.1)
buffer.writeBytes([127])
XCTAssertThrowsError(try buffer.readSSHMessage())
let message = try buffer.readSSHMessage()
guard case .some(.ignore) = message else {
XCTFail("Expected .ignore for unknown message type")
return
}
}

func testRequestSuccess() throws {
Expand Down Expand Up @@ -797,4 +802,53 @@ final class SSHMessagesTests: XCTestCase {

try self.assertCorrectlyManagesPartialRead(message)
}

func testUnknownMessageTypeReturnsIgnore() throws {
// This test verifies that unknown SSH message types are handled gracefully
// by returning .ignore() instead of throwing an error (per RFC 4253 Section 11.1).
var buffer = ByteBufferAllocator().buffer(capacity: 100)

// Use message ID 127 (reserved for local/private use per RFC 4253)
buffer.writeInteger(UInt8(127))
buffer.writeSSHString("extension-data".utf8)

let message = try buffer.readSSHMessage()

guard case .some(.ignore) = message else {
XCTFail("Expected .ignore for unknown message type, got \(String(describing: message))")
return
}

// Verify buffer is fully consumed
XCTAssertEqual(buffer.readableBytes, 0, "Parser should consume all bytes for unknown message type")
}

func testParserStateAfterUnknownMessageType() throws {
// This test verifies that the parser state remains valid after encountering
// an unknown message type, ensuring no corruption of sequence numbers or
// encryption state.
var buffer = ByteBufferAllocator().buffer(capacity: 200)

// Write unknown message type (ID 125 - reserved for local use)
buffer.writeInteger(UInt8(125))
buffer.writeBytes([1, 2, 3, 4]) // Some payload

// Read unknown message (should return .ignore)
let message1 = try buffer.readSSHMessage()
guard case .some(.ignore) = message1 else {
XCTFail("Expected .ignore for unknown message type")
return
}

// Write known message
buffer.writeInteger(SSHMessage.ChannelSuccessMessage.id)
buffer.writeInteger(UInt32(0))

// Verify parser continues correctly
let message2 = try buffer.readSSHMessage()
guard case .some(.channelSuccess) = message2 else {
XCTFail("Parser state corrupted after unknown message type - expected channelSuccess")
return
}
}
}