Files
RouterOS/RouterOSAssistantTests/RouterOSCommandBuilderTests.swift
KayandClaude Sonnet 5 800b48406b Fix live exploitierte RouterOS-CLI-Injection + Parser-Datenverlust
Deep-Dive-Gegencheck mit echtem Exploit-Nachweis gegen den Testrouter:

- RouterOSCommand.cliLine quotete Werte nur bei Leerzeichen und escapte
  eingebettete Anführungszeichen nie. Ein Kommentar wie test" ; :log
  warning "X schloss das Quoting vorzeitig und ließ RouterOS den Rest
  als zweiten Befehl ausführen. Live exploitiert (injizierter
  script,warning-Log-Eintrag) und live als behoben bestätigt. Betraf
  jede Schreiboperation über SSH - auf dem aktuellen Testrouter ist
  www-ssl deaktiviert, REST also unerreichbar, der Bug war aktiv.
- RouterOSCliParser.keyValues nahm an, print terse quote mehrwortige
  Werte - live an zwei Menüs widerlegt (RouterOS 7.24.4 quotet dort
  nichts). Trunkierte jeden mehrwortigen Wert beim ersten Leerzeichen.
  Fix: Token-Scan statt Regex.
- SSHTransport.fetchFieldValues defensiv gegen dieselbe Injection-Klasse
  gehärtet (aktuell nur hartkodiert aufgerufen, aber generische API).

2 neue Regressionstests, alle 101 Unit-Tests grün. Details in bugs.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 18:08:34 +02:00

147 lines
5.9 KiB
Swift

import XCTest
@testable import RouterOSAssistant
final class RouterOSCommandBuilderTests: XCTestCase {
func testWanDhcpClientCommand() {
let config = WanConfig(interfaceName: "ether1", mode: .dhcpClient)
let commands = config.buildCommands()
XCTAssertEqual(commands.count, 1)
XCTAssertEqual(commands[0].menuPath, "/ip dhcp-client")
XCTAssertEqual(commands[0].operation, .add)
XCTAssertEqual(commands[0].arguments["interface"], "ether1")
}
func testWanStaticIPCommandsIncludeRouteOnlyWhenGatewayGiven() {
var config = WanConfig(interfaceName: "ether1", mode: .staticIP)
config.staticAddress = "203.0.113.5/24"
XCTAssertEqual(config.buildCommands().count, 1)
config.staticGateway = "203.0.113.1"
let commands = config.buildCommands()
XCTAssertEqual(commands.count, 2)
XCTAssertEqual(commands[0].arguments["address"], "203.0.113.5/24")
XCTAssertEqual(commands[1].arguments["gateway"], "203.0.113.1")
}
func testWanPppoeCommand() {
var config = WanConfig(interfaceName: "ether1", mode: .pppoe)
config.pppoeUsername = "user@isp"
config.pppoePassword = "secret"
let commands = config.buildCommands()
XCTAssertEqual(commands.count, 1)
XCTAssertEqual(commands[0].menuPath, "/interface pppoe-client")
XCTAssertEqual(commands[0].arguments["user"], "user@isp")
XCTAssertEqual(commands[0].arguments["password"], "secret")
}
func testLanDhcpCommandsCoverAddressPoolServerAndNetwork() {
// Address fields default to "" now (Nutzerwunsch, 2026-09-16: fields start empty,
// showing only the format example) — set explicitly here so this test keeps verifying
// the command-building logic itself, not incidentally the model's old default values.
var config = LanDhcpConfig()
config.networkAddress = "192.168.88.0/24"
config.routerAddress = "192.168.88.1/24"
config.poolRangeStart = "192.168.88.10"
config.poolRangeEnd = "192.168.88.254"
config.dnsServers = "192.168.88.1"
let commands = config.buildCommands()
XCTAssertEqual(commands.count, 6)
XCTAssertEqual(commands[0].menuPath, "/ip address")
XCTAssertEqual(commands[1].menuPath, "/ip pool")
XCTAssertEqual(commands[2].menuPath, "/ip dhcp-server")
XCTAssertEqual(commands[3].menuPath, "/ip dhcp-server network")
XCTAssertEqual(commands[3].arguments["gateway"], "192.168.88.1")
XCTAssertEqual(commands[4].menuPath, "/interface list")
XCTAssertEqual(commands[4].arguments["name"], "LAN")
XCTAssertEqual(commands[5].menuPath, "/interface list member")
XCTAssertEqual(commands[5].arguments["list"], "LAN")
XCTAssertEqual(commands[5].arguments["interface"], "bridge")
}
func testLanDhcpCommandsDetachInterfaceFromBridgeWhenNotDefaultBridge() {
var config = LanDhcpConfig()
config.interfaceName = "ether4"
let commands = config.buildCommands()
XCTAssertEqual(commands.count, 7)
XCTAssertEqual(commands[0].menuPath, "/interface bridge port")
XCTAssertEqual(commands[0].operation, .remove(matchField: "interface", matchValue: "ether4"))
XCTAssertEqual(commands[1].menuPath, "/ip address")
}
func testCliLineRendersEmptyValueAsExplicitEmptyQuotes() {
let command = RouterOSCommand.set(
menuPath: "/ip route",
restPath: "ip/route",
matchField: ".id",
matchValue: "*1",
arguments: ["comment": ""],
summary: "test"
)
XCTAssertEqual(command.cliLine, "/ip route set [find .id=\"*1\"] comment=\"\"")
}
func testCliLineRendersSortedQuotedArgumentsForAdd() {
let command = RouterOSCommand.add(
menuPath: "/interface pppoe-client",
restPath: "interface/pppoe-client",
arguments: ["user": "user@isp", "password": "a secret"],
summary: "test"
)
XCTAssertEqual(command.cliLine, "/interface pppoe-client add password=\"a secret\" user=\"user@isp\"")
}
func testCliLineRendersFindLookupForSet() {
let command = RouterOSCommand.set(
menuPath: "/interface wireless",
restPath: "interface/wireless",
matchField: "name",
matchValue: "wlan1",
arguments: ["ssid": "Home"],
summary: "test"
)
XCTAssertEqual(command.cliLine, "/interface wireless set [find name=\"wlan1\"] ssid=\"Home\"")
}
func testCliLineRendersFindLookupForAction() {
let command = RouterOSCommand.action(
menuPath: "/ip dhcp-server lease",
restPath: "ip/dhcp-server/lease",
name: "make-static",
matchField: ".id",
matchValue: "*7",
summary: "test"
)
XCTAssertEqual(command.cliLine, "/ip dhcp-server lease make-static [find .id=\"*7\"]")
}
/// Regression test for bugs.md #1 (2026-09-17, "erneuter Gegencheck"-Durchgang): a free-text
/// value containing an embedded `"` followed by `;` used to close the CLI argument's quoting
/// early and let RouterOS' console treat the rest as a second, independent command —
/// live-confirmed exploitable (`:log warning "..."` executed as its own command via a comment
/// field) and live-confirmed fixed by escaping `\`/`"` and always quoting.
func testCliLineEscapesEmbeddedQuotesPreventingCommandInjection() {
let command = RouterOSCommand.add(
menuPath: "/ip firewall address-list",
restPath: "ip/firewall/address-list",
arguments: ["comment": "test\" ; :log warning \"INJECTED"],
summary: "test"
)
XCTAssertEqual(
command.cliLine,
"/ip firewall address-list add comment=\"test\\\" ; :log warning \\\"INJECTED\""
)
}
}