Skip to content

fix: interaction data corruption causing unmarshal errors#1341

Open
dogancanbakir wants to merge 1 commit intodevfrom
fix/interaction-unmarshal-corruption
Open

fix: interaction data corruption causing unmarshal errors#1341
dogancanbakir wants to merge 1 commit intodevfrom
fix/interaction-unmarshal-corruption

Conversation

@dogancanbakir
Copy link
Member

@dogancanbakir dogancanbakir commented Feb 26, 2026

Fixes #1340

  • Fix OnCacheRemovalCallback type assertion (value.([]byte)key.(string)) so LevelDB entries are cleaned on cache eviction
  • Clear stale LevelDB data in SetIDPublicKey before creating new cache entry on re-registration
  • Replace jsoniter.NewEncoder().Encode() with jsoniter.Marshal() across all server protocol handlers to avoid trailing \n in encrypted data
  • Add defensive bytes.TrimRight in client before unmarshal

Summary by CodeRabbit

  • Bug Fixes

    • Fixed protocol detection to correctly identify HTTPS connections instead of defaulting to HTTP
    • Improved message parsing by trimming trailing whitespace
  • Tests

    • Added comprehensive encryption and storage round-trip test coverage
    • Added protocol detection test cases
  • Chores

    • Updated dependencies to latest versions
    • Optimized JSON encoding performance
    • Enhanced cache eviction and data cleanup on re-registration

@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Feb 26, 2026

Neo - PR Security Review

No security issues found

Highlights

  • Fixes cache removal callback type assertion bug that prevented LevelDB cleanup on eviction
  • Adds stale data cleanup in SetIDPublicKey to prevent decryption failures after re-registration
  • Replaces jsoniter.NewEncoder().Encode() with jsoniter.Marshal() across all protocol handlers to eliminate trailing newlines in encrypted data
  • Adds defensive bytes.TrimRight() in client before unmarshal to handle whitespace
Hardening Notes
  • The cache removal callback fix (storagedb.go:76-80) now correctly uses key.(string) instead of value.([]byte), preventing resource leaks when cache entries expire
  • The stale data cleanup (storagedb.go:127-129) prevents a scenario where old encrypted data (encrypted with a previous AES key) would cause client-side decryption failures after session restore
  • The switch from NewEncoder().Encode() to Marshal() eliminates trailing newlines that were being encrypted and causing unmarshal errors on the client side
  • The defensive TrimRight in client.go:462 provides backward compatibility and resilience against any future whitespace issues in the encryption pipeline

Comment @neo help for available commands. · Open in Neo

@dogancanbakir dogancanbakir changed the base branch from main to dev February 26, 2026 14:04
@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Walkthrough

This PR refactors JSON encoding across server modules by replacing jsoniter.NewEncoder with jsoniter.Marshal to eliminate trailing newlines, fixes cache eviction cleanup in storage to prevent stale encrypted data persistence, adds whitespace trimming to client decryption, introduces HTTP protocol detection, and includes comprehensive encryption/decryption round-trip tests.

Changes

Cohort / File(s) Summary
Server JSON encoding refactoring
pkg/server/dns_server.go, pkg/server/ftp_server.go, pkg/server/ldap_server.go, pkg/server/responder_server.go, pkg/server/smb_server.go, pkg/server/smtp_server.go
Replaced jsoniter.NewEncoder with jsoniter.Marshal to produce byte slices directly, removing bytes.Buffer dependency. Updated logging to use string(data) instead of buffer.String() and storage calls to pass marshaled bytes directly.
HTTP protocol detection and request handling
pkg/server/http_server.go, pkg/server/http_server_test.go
Added httpProtocol() helper to determine "https" vs "http" based on TLS presence. Updated handleInteraction() signature to accept *http.Request as first parameter. Replaced buffer-based encoding with jsoniter.Marshal. New unit tests verify protocol detection for plaintext and TLS scenarios.
Client decryption whitespace normalization
pkg/client/client.go
Added trimming of trailing whitespace (spaces, tabs, carriage returns, newlines) from decrypted plaintext before unmarshalling into server.Interaction.
Storage cache cleanup and management
pkg/storage/storagedb.go
Fixed OnCacheRemovalCallback to treat cache key as string and delete corresponding LevelDB entries. Added conditional disk cleanup in SetIDPublicKey to purge stale encrypted data when re-registering the same correlation ID.
Encryption round-trip test suite
pkg/storage/roundtrip_test.go
Added comprehensive test file with in-memory and disk-based encryption/decryption flows: TestFullRoundTripInMemory, TestFullRoundTripDisk, TestPollResponseRoundTrip, TestTrailingNewlineHandling, TestJsoniterControlCharacterEscaping, and TestStaleDataCleanupOnReRegistration. Includes RSA key generation and AES decryption utilities.
Dependency updates
go.mod
Updated gologger, retryablehttp-go, and indirect dependencies (fastdialer, hmap) to latest patch versions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit hops through bytes with glee, 🐰
No trailing newlines now to see,
The cache is cleaned, stale data gone,
Each key unlocks the right datum,
Round-trips verified, control restored!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: interaction data corruption causing unmarshal errors' directly and accurately describes the main issue being resolved (issue #1340 regarding data corruption and unmarshal errors), matching the primary objective of the pull request.
Linked Issues check ✅ Passed The PR addresses all root causes identified in issue #1340: fixes OnCacheRemovalCallback type assertion [storagedb.go], clears stale LevelDB data on re-registration [storagedb.go], replaces jsoniter.NewEncoder with jsoniter.Marshal to avoid trailing newlines [dns_server.go, ftp_server.go, http_server.go, ldap_server.go, responder_server.go, smb_server.go, smtp_server.go], and adds defensive TrimRight in client [client.go].
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue: storage fixes, JSON encoding refactoring, protocol detection improvements, and defensive client trimming. Dependency updates in go.mod and test additions are supporting changes that align with the PR's scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/interaction-unmarshal-corruption

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (8)
pkg/server/smb_server.go (1)

103-111: Encoding change is correct.

The jsoniter.Marshal() approach properly avoids the trailing newline issue.

Same minor nit as responder_server.go: Line 109 has an incorrect error message saying "dns interaction" instead of "smb interaction".

🔧 Fix error message
 					if err := h.options.Storage.AddInteractionWithId(h.options.Token, data); err != nil {
-						gologger.Warning().Msgf("Could not store dns interaction: %s\n", err)
+						gologger.Warning().Msgf("Could not store smb interaction: %s\n", err)
 					}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/server/smb_server.go` around lines 103 - 111, The warning message uses
the wrong protocol name; when jsoniter.Marshal(interaction) succeeds and
h.options.Storage.AddInteractionWithId(h.options.Token, data) returns an error,
update the gologger.Warning().Msgf call to reference "smb interaction" instead
of "dns interaction" (i.e., adjust the message text in the AddInteractionWithId
error handling branch so it correctly says "Could not store smb interaction:
%s\n").
pkg/server/responder_server.go (1)

91-99: Encoding change looks correct.

The switch from jsoniter.NewEncoder().Encode() to jsoniter.Marshal() properly eliminates the trailing newline that was causing unmarshal errors. The Marshal function returns compact JSON without a trailing newline.

Minor nit: Line 97 has an incorrect error message saying "dns interaction" instead of "responder interaction".

🔧 Fix error message
 					if err := h.options.Storage.AddInteractionWithId(h.options.Token, data); err != nil {
-						gologger.Warning().Msgf("Could not store dns interaction: %s\n", err)
+						gologger.Warning().Msgf("Could not store responder interaction: %s\n", err)
 					}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/server/responder_server.go` around lines 91 - 99, Update the log message
string in the interaction storage error to correctly refer to "responder
interaction" instead of "dns interaction": locate the block that marshals the
interaction (calls jsoniter.Marshal(interaction)) and then calls
h.options.Storage.AddInteractionWithId(h.options.Token, data); change the
gologger.Warning().Msgf call that currently says "Could not store dns
interaction: %s" to "Could not store responder interaction: %s" so the message
accurately reflects the operation and use the same error variable.
pkg/storage/roundtrip_test.go (5)

173-178: Same encoding inconsistency in disk test.

Same suggestion as above: consider using jsoniter.Marshal() to match production code.

♻️ Suggested change
-		buffer := &bytes.Buffer{}
-		err = jsoniter.NewEncoder(buffer).Encode(inter)
-		require.NoError(t, err, "encode interaction %d", i)
-
-		err = db.AddInteraction(correlationID, buffer.Bytes())
+		data, err := jsoniter.Marshal(inter)
+		require.NoError(t, err, "encode interaction %d", i)
+
+		err = db.AddInteraction(correlationID, data)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/storage/roundtrip_test.go` around lines 173 - 178, The test uses
jsoniter.NewEncoder(buffer).Encode(inter) to produce the bytes fed into
db.AddInteraction(correlationID, buffer.Bytes()), causing encoding differences
from production; replace the encoder usage with jsoniter.Marshal(inter) and pass
that result into AddInteraction so the test serializes interactions exactly like
production code (look for variables/identifiers buffer, inter,
jsoniter.NewEncoder, and db.AddInteraction).

119-124: Test uses Encoder.Encode() but production code now uses Marshal().

The production code (e.g., http_server.go, dns_server.go) was changed to use jsoniter.Marshal() to avoid trailing newlines, but this test still uses jsoniter.NewEncoder(buffer).Encode(). While Unmarshal tolerates trailing newlines, using the same encoding method as production would make this test more representative of the actual code path.

♻️ Suggested change to match production code
-		buffer := &bytes.Buffer{}
-		err = jsoniter.NewEncoder(buffer).Encode(inter)
-		require.NoError(t, err, "encode interaction %d", i)
-
-		err = mem.AddInteraction(correlationID, buffer.Bytes())
+		data, err := jsoniter.Marshal(inter)
+		require.NoError(t, err, "encode interaction %d", i)
+
+		err = mem.AddInteraction(correlationID, data)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/storage/roundtrip_test.go` around lines 119 - 124, The test currently
uses jsoniter.NewEncoder(buffer).Encode(inter) which writes a trailing newline
unlike production; update the test to use jsoniter.Marshal(inter) and pass the
resulting bytes to mem.AddInteraction(correlationID, data) (handling the
returned bytes and error inline) so the test encodes interactions the same way
as production code paths that use jsoniter.Marshal().

353-358: Test relies on timing for cache eviction - potential flakiness.

The test sleeps for 200ms to wait for cache eviction with a 100ms TTL. While this should generally work, timing-based tests can be flaky under CI load. The assertion require.False(t, found, "cache entry should be evicted") helps catch this, but if it becomes flaky, consider:

  1. Increasing the sleep margin
  2. Using a retry loop to poll for eviction

This is minor since the current margin (2x the TTL) is reasonable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/storage/roundtrip_test.go` around lines 353 - 358, The test in
roundtrip_test.go relies on time.Sleep(200 * time.Millisecond) to wait for a
100ms TTL eviction which can be flaky; replace this fixed sleep with a short
polling/retry loop that calls db.cache.GetIfPresent(correlationID) until it
returns not found or a reasonable timeout elapses (e.g., 500–1000ms), or
alternatively increase the sleep margin; update the assertion to fail only after
the timeout so GetIfPresent is polled rather than assumed evicted after a single
sleep.

230-234: Same encoding inconsistency in PollResponse test.

Consider using jsoniter.Marshal() here as well for consistency with production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/storage/roundtrip_test.go` around lines 230 - 234, In the PollResponse
test in roundtrip_test.go, replace the jsoniter.NewEncoder usage with
jsoniter.Marshal for consistency with production: instead of creating buffer :=
&bytes.Buffer{} and calling jsoniter.NewEncoder(buffer).Encode(inter) then
mem.AddInteraction(correlationID, buffer.Bytes()), call jsoniter.Marshal(inter)
to get the payload bytes and pass those to mem.AddInteraction(correlationID,
payload) so the test uses jsoniter.Marshal like other tests.

261-286: Test validates encoder behavior but production no longer uses encoder.

This test explicitly verifies that Encoder.Encode() appends a trailing newline. However, since production code now uses Marshal() (which doesn't append a newline), this test documents historical behavior rather than current production behavior.

Consider either:

  1. Updating the test to verify Marshal() does not append a trailing newline (the actual production behavior)
  2. Renaming/documenting it as a regression test to ensure Unmarshal tolerates trailing newlines if encountered
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/storage/roundtrip_test.go` around lines 261 - 286,
TestTrailingNewlineHandling currently asserts Encoder.Encode appends a trailing
newline but production uses jsoniter.Marshal; update the test to call
jsoniter.Marshal on the interaction (or otherwise use Marshal instead of
Encoder.Encode), assert that the resulting byte slice does NOT end with '\n'
(reflecting current production behavior), and then (optionally) keep a small
regression subtest that feeds an encoder-produced payload to jsoniter.Unmarshal
to ensure Unmarshal tolerates trailing newlines; reference the interaction
struct, jsoniter.Marshal/jsoniter.Unmarshal and TestTrailingNewlineHandling when
making changes.
pkg/client/client.go (1)

456-468: Whitespace trimming is fine, but likely redundant vs JSON whitespace handling; consider simplifying and/or clarifying intent.

bytes.TrimRight(plaintext, " \t\r\n") is safe as a defensive normalization, but it’s worth confirming whether the client actually needs this now that the server moved away from Encoder.Encode() (newline) and because JSON parsers typically tolerate trailing whitespace anyway. If the goal is purely “drop encoder newline”, consider using the idiomatic bytes.TrimSpace for clarity.

If you still need to tolerate non-whitespace trailing bytes, this approach won’t address that—so I’d either (a) keep this strictly as “whitespace normalization” and add a short comment, or (b) add an explicit fallback path with clear logging so corruption isn’t silently masked.

♻️ Possible simplification
-        plaintext = bytes.TrimRight(plaintext, " \t\r\n")
+        // Normalize trailing whitespace (e.g. legacy encoder newline) before JSON unmarshal.
+        plaintext = bytes.TrimSpace(plaintext)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client/client.go` around lines 456 - 468, The bytes.TrimRight call in the
loop over response.Data is defensive whitespace normalization but is redundant
given JSON parsers tolerate trailing whitespace; replace
bytes.TrimRight(plaintext, " \t\r\n") with bytes.TrimSpace(plaintext) and add a
one-line comment above it explaining this is purely defensive whitespace
normalization (not an attempt to mask non-whitespace corruption). Keep the rest
of the flow (decryptMessage, jsoniter.Unmarshal, callback) unchanged; if you
actually need to tolerate non-whitespace trailing bytes instead, add an explicit
fallback branch that logs a warning before attempting a more lenient parse so
corruption isn’t silently masked.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/client/client.go`:
- Around line 456-468: The bytes.TrimRight call in the loop over response.Data
is defensive whitespace normalization but is redundant given JSON parsers
tolerate trailing whitespace; replace bytes.TrimRight(plaintext, " \t\r\n") with
bytes.TrimSpace(plaintext) and add a one-line comment above it explaining this
is purely defensive whitespace normalization (not an attempt to mask
non-whitespace corruption). Keep the rest of the flow (decryptMessage,
jsoniter.Unmarshal, callback) unchanged; if you actually need to tolerate
non-whitespace trailing bytes instead, add an explicit fallback branch that logs
a warning before attempting a more lenient parse so corruption isn’t silently
masked.

In `@pkg/server/responder_server.go`:
- Around line 91-99: Update the log message string in the interaction storage
error to correctly refer to "responder interaction" instead of "dns
interaction": locate the block that marshals the interaction (calls
jsoniter.Marshal(interaction)) and then calls
h.options.Storage.AddInteractionWithId(h.options.Token, data); change the
gologger.Warning().Msgf call that currently says "Could not store dns
interaction: %s" to "Could not store responder interaction: %s" so the message
accurately reflects the operation and use the same error variable.

In `@pkg/server/smb_server.go`:
- Around line 103-111: The warning message uses the wrong protocol name; when
jsoniter.Marshal(interaction) succeeds and
h.options.Storage.AddInteractionWithId(h.options.Token, data) returns an error,
update the gologger.Warning().Msgf call to reference "smb interaction" instead
of "dns interaction" (i.e., adjust the message text in the AddInteractionWithId
error handling branch so it correctly says "Could not store smb interaction:
%s\n").

In `@pkg/storage/roundtrip_test.go`:
- Around line 173-178: The test uses jsoniter.NewEncoder(buffer).Encode(inter)
to produce the bytes fed into db.AddInteraction(correlationID, buffer.Bytes()),
causing encoding differences from production; replace the encoder usage with
jsoniter.Marshal(inter) and pass that result into AddInteraction so the test
serializes interactions exactly like production code (look for
variables/identifiers buffer, inter, jsoniter.NewEncoder, and
db.AddInteraction).
- Around line 119-124: The test currently uses
jsoniter.NewEncoder(buffer).Encode(inter) which writes a trailing newline unlike
production; update the test to use jsoniter.Marshal(inter) and pass the
resulting bytes to mem.AddInteraction(correlationID, data) (handling the
returned bytes and error inline) so the test encodes interactions the same way
as production code paths that use jsoniter.Marshal().
- Around line 353-358: The test in roundtrip_test.go relies on time.Sleep(200 *
time.Millisecond) to wait for a 100ms TTL eviction which can be flaky; replace
this fixed sleep with a short polling/retry loop that calls
db.cache.GetIfPresent(correlationID) until it returns not found or a reasonable
timeout elapses (e.g., 500–1000ms), or alternatively increase the sleep margin;
update the assertion to fail only after the timeout so GetIfPresent is polled
rather than assumed evicted after a single sleep.
- Around line 230-234: In the PollResponse test in roundtrip_test.go, replace
the jsoniter.NewEncoder usage with jsoniter.Marshal for consistency with
production: instead of creating buffer := &bytes.Buffer{} and calling
jsoniter.NewEncoder(buffer).Encode(inter) then mem.AddInteraction(correlationID,
buffer.Bytes()), call jsoniter.Marshal(inter) to get the payload bytes and pass
those to mem.AddInteraction(correlationID, payload) so the test uses
jsoniter.Marshal like other tests.
- Around line 261-286: TestTrailingNewlineHandling currently asserts
Encoder.Encode appends a trailing newline but production uses jsoniter.Marshal;
update the test to call jsoniter.Marshal on the interaction (or otherwise use
Marshal instead of Encoder.Encode), assert that the resulting byte slice does
NOT end with '\n' (reflecting current production behavior), and then
(optionally) keep a small regression subtest that feeds an encoder-produced
payload to jsoniter.Unmarshal to ensure Unmarshal tolerates trailing newlines;
reference the interaction struct, jsoniter.Marshal/jsoniter.Unmarshal and
TestTrailingNewlineHandling when making changes.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 307efeb and da91f8e.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • go.mod
  • pkg/client/client.go
  • pkg/server/dns_server.go
  • pkg/server/ftp_server.go
  • pkg/server/http_server.go
  • pkg/server/http_server_test.go
  • pkg/server/ldap_server.go
  • pkg/server/responder_server.go
  • pkg/server/smb_server.go
  • pkg/server/smtp_server.go
  • pkg/storage/roundtrip_test.go
  • pkg/storage/storagedb.go

@knakul853
Copy link

@dogancanbakir TestFullRoundTripInMemory and TestFullRoundTripDisk these test still using older decoder logic good to update to new logic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Interaction data corruption causes unmarshal errors on client

2 participants