Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resovle host to all ips and check against the deny list #964

Merged
merged 3 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@

package org.opensearch.notifications.spi.utils

import inet.ipaddr.HostName
import inet.ipaddr.IPAddressString
import org.apache.commons.validator.routines.DomainValidator
import org.apache.hc.client5.http.classic.methods.HttpPatch
import org.apache.hc.client5.http.classic.methods.HttpPost
import org.apache.hc.client5.http.classic.methods.HttpPut
import org.apache.logging.log4j.LogManager
import org.opensearch.core.common.Strings
import org.opensearch.notifications.spi.utils.ValidationHelpers.FQDN_REGEX
import java.lang.Exception
import java.net.InetAddress
import java.net.URL

Expand Down Expand Up @@ -49,13 +52,38 @@ fun isValidUrl(urlString: String): Boolean {
}
}

fun getResolvedIps(host: String): List<IPAddressString> {
try {
val resolvedIps = InetAddress.getAllByName(host)
return resolvedIps.map { inetAddress -> IPAddressString(inetAddress.hostAddress) }
} catch (e: Exception) {
LogManager.getLogger().error("Unable to resolve host ips")
}

return listOf()
}

fun isHostInDenylist(urlString: String, hostDenyList: List<String>): Boolean {
val url = URL(urlString)
if (url.host != null) {
val ipStr = IPAddressString(InetAddress.getByName(url.host).hostAddress)
val resolvedIpStrings = getResolvedIps(url.host)
val hostStr = HostName(url.host)

for (network in hostDenyList) {
val netStr = IPAddressString(network)
if (netStr.contains(ipStr)) {
val denyIpStr = IPAddressString(network)
val denyHostStr = HostName(network)
val hostInDenyList = denyHostStr.equals(hostStr)
var ipInDenyList = false

for (ipStr in resolvedIpStrings) {
if (denyIpStr.contains(ipStr)) {
ipInDenyList = true
break
}
}

if (hostInDenyList || ipInDenyList) {
LogManager.getLogger().error("${url.host} is denied")
return true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,21 @@ internal class ValidationHelpersTests {

@Test
fun `test hostname gets resolved to ip for denylist`() {
val invalidHost = "invalid.com"
val expectedAddressesForInvalidHost = arrayOf(
InetAddress.getByName("174.120.0.0"),
InetAddress.getByName("10.0.0.1")
)
val expectedAddressesForValidHost = arrayOf(
InetAddress.getByName("174.12.0.0")
)

mockkStatic(InetAddress::class)
every { InetAddress.getByName(invalidHost).hostAddress } returns "10.0.0.1" // 10.0.0.0/8
val invalidHost = "invalid.com"
every { InetAddress.getAllByName(invalidHost) } returns expectedAddressesForInvalidHost
assertEquals(true, isHostInDenylist("https://$invalidHost", hostDenyList))

val validHost = "valid.com"
every { InetAddress.getByName(validHost).hostAddress } returns "174.12.0.0"
every { InetAddress.getAllByName(validHost) } returns expectedAddressesForValidHost
assertEquals(false, isHostInDenylist("https://$validHost", hostDenyList))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,22 @@ import org.apache.hc.client5.http.classic.methods.HttpPost
import org.apache.hc.client5.http.classic.methods.HttpPut
import org.apache.logging.log4j.LogManager
import org.opensearch.core.common.Strings
import java.lang.Exception
import java.net.InetAddress
import java.net.URL
import java.net.UnknownHostException

fun validateUrl(urlString: String) {
require(!Strings.isNullOrEmpty(urlString)) { "url is null or empty" }
require(isValidUrl(urlString)) { "Invalid URL or unsupported" }
}

fun validateUrlHost(urlString: String, hostDenyList: List<String>) {
require(!isHostInDenylist(urlString, hostDenyList)) {
val url = URL(urlString)
require(org.opensearch.notifications.spi.utils.getResolvedIps(url.host).isNotEmpty()) {
"Host could not be resolved to a valid Ip address"
}
require(!org.opensearch.notifications.spi.utils.isHostInDenylist(urlString, hostDenyList)) {
"Host of url is denied, based on plugin setting [notification.core.http.host_deny_list]"
}
}
Expand All @@ -35,18 +42,39 @@ fun isValidUrl(urlString: String): Boolean {
return ("https" == url.protocol || "http" == url.protocol) // Support only http/https, other protocols not supported
}

@Deprecated("This function is not maintained, use org.opensearch.notifications.spi.utils.isHostInDenylist instead.")
fun isHostInDenylist(urlString: String, hostDenyList: List<String>): Boolean {
val url = URL(urlString)
if (url.host != null) {
val ipStr = IPAddressString(url.host)
val hostStr = HostName(url.host)
for (network in hostDenyList) {
val denyIpStr = IPAddressString(network)
val denyHostStr = HostName(network)
if (denyIpStr.contains(ipStr) || denyHostStr.equals(hostStr)) {
LogManager.getLogger().error("${url.host} is denied")
return true
try {
val resolvedIps = InetAddress.getAllByName(url.host)
val resolvedIpStrings = resolvedIps.map { inetAddress -> IPAddressString(inetAddress.hostAddress) }
val hostStr = HostName(url.host)

for (network in hostDenyList) {
val denyIpStr = IPAddressString(network)
val denyHostStr = HostName(network)
val hostInDenyList = denyHostStr.equals(hostStr)
var ipInDenyList = false

for (ipStr in resolvedIpStrings) {
if (denyIpStr.contains(ipStr)) {
ipInDenyList = true
break
}
}

if (hostInDenyList || ipInDenyList) {
LogManager.getLogger().error("${url.host} is denied")
return true
}
}
} catch (e: UnknownHostException) {
LogManager.getLogger().error("Error checking denylist: Unknown host")
return false
} catch (e: Exception) {
LogManager.getLogger().error("Error checking denylist: ${e.message}", e)
return false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package org.opensearch.notifications.core.destinations

import inet.ipaddr.IPAddressString
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
Expand Down Expand Up @@ -50,6 +51,7 @@ internal class ChimeDestinationTests {
// Stubbing isHostInDenylist() so it doesn't attempt to resolve hosts that don't exist in the unit tests
mockkStatic("org.opensearch.notifications.spi.utils.ValidationHelpersKt")
every { org.opensearch.notifications.spi.utils.isHostInDenylist(any(), any()) } returns false
every { org.opensearch.notifications.spi.utils.getResolvedIps(any()) } returns listOf(IPAddressString("174.0.0.0"))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package org.opensearch.notifications.core.destinations

import inet.ipaddr.IPAddressString
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
Expand Down Expand Up @@ -62,6 +63,7 @@ internal class CustomWebhookDestinationTests {
// Stubbing isHostInDenylist() so it doesn't attempt to resolve hosts that don't exist in the unit tests
mockkStatic("org.opensearch.notifications.spi.utils.ValidationHelpersKt")
every { org.opensearch.notifications.spi.utils.isHostInDenylist(any(), any()) } returns false
every { org.opensearch.notifications.spi.utils.getResolvedIps(any()) } returns listOf(IPAddressString("174.0.0.0"))
}

@ParameterizedTest(name = "method {0} should return corresponding type of Http request object {1}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package org.opensearch.notifications.core.destinations

import inet.ipaddr.IPAddressString
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
Expand Down Expand Up @@ -52,6 +53,7 @@ internal class MicrosoftTeamsDestinationTests {
// Stubbing isHostInDenylist() so it doesn't attempt to resolve hosts that don't exist in the unit tests
mockkStatic("org.opensearch.notifications.spi.utils.ValidationHelpersKt")
every { org.opensearch.notifications.spi.utils.isHostInDenylist(any(), any()) } returns false
every { org.opensearch.notifications.spi.utils.getResolvedIps(any()) } returns listOf(IPAddressString("174.0.0.0"))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package org.opensearch.notifications.core.destinations

import inet.ipaddr.IPAddressString
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
Expand Down Expand Up @@ -52,6 +53,7 @@ internal class SlackDestinationTests {
// Stubbing isHostInDenylist() so it doesn't attempt to resolve hosts that don't exist in the unit tests
mockkStatic("org.opensearch.notifications.spi.utils.ValidationHelpersKt")
every { org.opensearch.notifications.spi.utils.isHostInDenylist(any(), any()) } returns false
every { org.opensearch.notifications.spi.utils.getResolvedIps(any()) } returns listOf(IPAddressString("174.0.0.0"))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

package org.opensearch.notifications.core.utils

import io.mockk.every
import io.mockk.mockkStatic
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.net.InetAddress

internal class ValidationHelpersTests {

Expand Down Expand Up @@ -44,4 +47,24 @@ internal class ValidationHelpersTests {
assertEquals(false, isHostInDenylist("https://$url", hostDenyList), "address $url was not supposed to be identified as in the deny list, but was")
}
}

@Test
fun `test hostname gets resolved to ip for denylist`() {
val expectedAddressesForInvalidHost = arrayOf(
InetAddress.getByName("174.120.0.0"),
InetAddress.getByName("10.0.0.1")
)
val expectedAddressesForValidHost = arrayOf(
InetAddress.getByName("174.12.0.0")
)

mockkStatic(InetAddress::class)
val invalidHost = "invalid.com"
every { InetAddress.getAllByName(invalidHost) } returns expectedAddressesForInvalidHost
assertEquals(true, isHostInDenylist("https://$invalidHost", hostDenyList))

val validHost = "valid.com"
every { InetAddress.getAllByName(validHost) } returns expectedAddressesForValidHost
assertEquals(false, isHostInDenylist("https://$validHost", hostDenyList))
}
}
Loading