From 58de78972f4c629e82a5c99a50b4093d0665d6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fan=20Can=20Bak=C4=B1r?= Date: Tue, 30 Jun 2026 19:01:14 +0300 Subject: [PATCH 1/2] fix(client): warn when server lacks IPv6 (AAAA) records --- README.md | 4 ++- cmd/interactsh-client/main.go | 14 +++++++++ pkg/client/ipv6.go | 58 +++++++++++++++++++++++++++++++++++ pkg/client/ipv6_test.go | 39 +++++++++++++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 pkg/client/ipv6.go create mode 100644 pkg/client/ipv6_test.go diff --git a/README.md b/README.md index 1d2af926..e67e4cf7 100644 --- a/README.md +++ b/README.md @@ -536,6 +536,8 @@ $ interactsh-server -d oast.pro -ip 192.0.2.1,2001:db8::1 The server will automatically detect and categorize IPv4 and IPv6 addresses, returning appropriate DNS records based on the query type. +When the selected server publishes no AAAA records, the client prints a warning so that interactions from IPv6-only sources are not silently missed and mistaken for the absence of a vulnerability. +
@@ -644,7 +646,7 @@ interactsh-server -d hackwithautomation.com -http-index banner.html Interactsh http server optionally enables file hosting to help in security testing. This capability can be used with a self-hosted server to serve files for common payloads for **XSS, XXE, RCE** and other attacks. -To use this feature, `-http-directory` flag can be used which accepts diretory as input and files are served under `/s/` directory. +To use this feature, `-http-directory` flag can be used which accepts directory as input and files are served under `/s/` directory. ```bash interactsh-server -d hackwithautomation.com -http-directory ./paylods diff --git a/cmd/interactsh-client/main.go b/cmd/interactsh-client/main.go index e25843af..d0556728 100644 --- a/cmd/interactsh-client/main.go +++ b/cmd/interactsh-client/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "encoding/json" "fmt" "os" @@ -177,6 +178,8 @@ func main() { gologger.Info().Msgf("%s\n", interactshURL) } + warnIfServerLacksIPv6(client) + if cliOptions.StorePayload && cliOptions.StorePayloadFile != "" { if err := os.WriteFile(cliOptions.StorePayloadFile, []byte(strings.Join(interactshURLs, "\n")), 0644); err != nil { gologger.Fatal().Msgf("Could not write to payload output file: %s\n", err) @@ -298,6 +301,17 @@ func main() { } } +// warnIfServerLacksIPv6 alerts the user when the chosen server publishes no +// AAAA records, since interactions reaching the target over IPv6 would +// otherwise be dropped silently and read as "no interaction" (issue #1391). +func warnIfServerLacksIPv6(c *client.Client) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if ok, err := c.ServerSupportsIPv6(ctx); err == nil && !ok { + gologger.Warning().Msgf("Server %s publishes no IPv6 (AAAA) records; interactions from IPv6-only sources will be missed\n", c.ServerURL()) + } +} + func generatePayloadURL(numberOfPayloads int, client *client.Client) []string { interactshURLs := make([]string, numberOfPayloads) for i := 0; i < numberOfPayloads; i++ { diff --git a/pkg/client/ipv6.go b/pkg/client/ipv6.go new file mode 100644 index 00000000..f3cef81c --- /dev/null +++ b/pkg/client/ipv6.go @@ -0,0 +1,58 @@ +package client + +import ( + "context" + "errors" + "net" + + iputil "github.com/projectdiscovery/utils/ip" +) + +// errServerNotResolvable is returned when the IPv6 capability of the server +// cannot be determined because it is addressed by a literal IP rather than a +// resolvable hostname. +var errServerNotResolvable = errors.New("server is not a resolvable hostname") + +// ipResolver resolves a host to its IP addresses. *net.Resolver satisfies it. +type ipResolver interface { + LookupIP(ctx context.Context, network, host string) ([]net.IP, error) +} + +// hostHasIPv6 reports whether host resolves to at least one IPv6 address. A +// false result with a nil error means the host resolves but publishes no AAAA +// records; a non-nil error means resolution failed and the result is unknown. +func hostHasIPv6(ctx context.Context, resolver ipResolver, host string) (bool, error) { + addrs, err := resolver.LookupIP(ctx, "ip", host) + if err != nil { + return false, err + } + for _, addr := range addrs { + if addr.To4() == nil && addr.To16() != nil { + return true, nil + } + } + return false, nil +} + +// ServerSupportsIPv6 reports whether the interactsh server in use publishes +// IPv6 (AAAA) records. A false result means interactions reaching the target +// over IPv6 are silently dropped by the server. The boolean is meaningful only +// when the returned error is nil. +func (c *Client) ServerSupportsIPv6(ctx context.Context) (bool, error) { + if c.serverURL == nil { + return false, errServerNotResolvable + } + host := c.serverURL.Hostname() + if host == "" || iputil.IsIP(host) { + return false, errServerNotResolvable + } + return hostHasIPv6(ctx, net.DefaultResolver, host) +} + +// ServerURL returns the interactsh server the client registered to. +func (c *Client) ServerURL() string { + if c.serverURL == nil { + return "" + } + return c.serverURL.String() +} diff --git a/pkg/client/ipv6_test.go b/pkg/client/ipv6_test.go new file mode 100644 index 00000000..044570d3 --- /dev/null +++ b/pkg/client/ipv6_test.go @@ -0,0 +1,39 @@ +package client + +import ( + "context" + "errors" + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +type stubResolver struct { + addrs []net.IP + err error +} + +func (s stubResolver) LookupIP(context.Context, string, string) ([]net.IP, error) { + return s.addrs, s.err +} + +func TestHostHasIPv6(t *testing.T) { + t.Run("ipv4 only reports no ipv6", func(t *testing.T) { + ok, err := hostHasIPv6(context.Background(), stubResolver{addrs: []net.IP{net.ParseIP("178.128.212.209")}}, "oast.pro") + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("dual stack reports ipv6", func(t *testing.T) { + ok, err := hostHasIPv6(context.Background(), stubResolver{addrs: []net.IP{net.ParseIP("178.128.212.209"), net.ParseIP("2001:db8::1")}}, "oast.pro") + require.NoError(t, err) + require.True(t, ok) + }) + + t.Run("resolution failure surfaces error", func(t *testing.T) { + ok, err := hostHasIPv6(context.Background(), stubResolver{err: errors.New("no such host")}, "oast.pro") + require.Error(t, err) + require.False(t, ok) + }) +} From 5aaa208769c959496334fc73368e57b09b7b5ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fan=20Can=20Bak=C4=B1r?= Date: Tue, 30 Jun 2026 19:04:57 +0300 Subject: [PATCH 2/2] fix(test): replace removed jsoniter with stdlib json --- pkg/server/http_server_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/server/http_server_test.go b/pkg/server/http_server_test.go index 44f51f57..f45e14a2 100644 --- a/pkg/server/http_server_test.go +++ b/pkg/server/http_server_test.go @@ -62,7 +62,7 @@ func TestWildcardHTTPSRequestWithPortIsStoredAsRootTLDInteraction(t *testing.T) require.Len(t, interactions, 1) var interaction Interaction - require.NoError(t, jsoniter.UnmarshalFromString(interactions[0], &interaction)) + require.NoError(t, json.Unmarshal([]byte(interactions[0]), &interaction)) require.Equal(t, "https", interaction.Protocol) require.Equal(t, "abc123.example.com:443", interaction.UniqueID) require.Equal(t, "abc123.example.com:443", interaction.FullId)