srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/proxy/tee.go
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-05-15 19:48:00 +0200
committersrdusr <[email protected]>2026-05-15 19:48:00 +0200
commitdd1621c0077e079e0693f16fe83f17d50338216e (patch)
tree411a06d723a16aff1077d5d3e724680436aac85a /internal/proxy/tee.go
parentd7d50ae9902d69b2e52d446684b5ab01a5ecab1e (diff)
downloadmitmux-dd1621c0077e079e0693f16fe83f17d50338216e.tar.gz
mitmux-dd1621c0077e079e0693f16fe83f17d50338216e.zip
Stop mislabeling truncated captures as "exact"
internal/proxy/tee.go's teeConn silently drops bytes past maxCaptureBytes (10 MiB) but callers unconditionally marked the result "exact" anyway. Confirmed live: proxying a 15 MiB response worked correctly end-to-end (the client got the full, real 15 MiB - proxying itself is unbounded, only storage is capped), but the stored history entry was exactly 10485760 bytes with response_exact=1 still set. For a tool whose core value proposition is "raw bytes are the source of truth," a silently truncated capture presented as complete could hide the very evidence a smuggling or parser-differential investigation is looking for in the tail of a large body - and give false confidence that it isn't there. teeConn.Take() now returns (data, truncated) instead of just data; truncated is true whenever a Read had to drop bytes because the buffer was already at cap. Every caller (proxy.go's forward() on both the request and response side, repeat.go's sendRaw for Repeater/Intruder) now folds truncated into exact - a truncated capture is never marked exact - and additionally threads a distinct RequestTruncated/ ResponseTruncated bool through store.Entry, ipc.EntryDetail, and the TUI, since "truncated" and "reconstructed" (HTTP/2, which never had wire-exact bytes to begin with) are different situations worth telling apart: a truncated capture is still real wire bytes, just incomplete, not a synthesized reconstruction. The detail/repeater views now show "truncated (hit capture size limit)" specifically rather than lumping it in with "reconstructed", which would have implied more transformation happened than actually did. Schema: history gains request_truncated/response_truncated columns via the same ALTER-TABLE-and-ignore-duplicate-column pattern already used for source/flagged, so existing databases upgrade in place. Verified live: a target server returning a 15 MiB body (over the 10 MiB cap) proxied through cleanly - full body reached the client - while the stored entry shows length=10485760, response_exact=0, response_truncated=1 (previously would have shown response_exact=1); the TUI's Detail view correctly displays "Response (10485760 bytes, truncated (hit capture size limit))" instead of "exact". go build/vet/gofmt/test/mod tidy all clean.
Diffstat (limited to 'internal/proxy/tee.go')
-rw-r--r--internal/proxy/tee.go29
1 files changed, 21 insertions, 8 deletions
diff --git a/internal/proxy/tee.go b/internal/proxy/tee.go
index f6a385a..bd34d19 100644
--- a/internal/proxy/tee.go
+++ b/internal/proxy/tee.go
@@ -22,8 +22,9 @@ const maxCaptureBytes = 10 << 20 // 10 MiB
// here (call sites synchronize on the request/response boundary itself).
type teeConn struct {
net.Conn
- mu sync.Mutex
- buf []byte
+ mu sync.Mutex
+ buf []byte
+ truncated bool
}
func newTeeConn(c net.Conn) *teeConn {
@@ -34,12 +35,18 @@ func (c *teeConn) Read(p []byte) (int, error) {
n, err := c.Conn.Read(p)
if n > 0 {
c.mu.Lock()
- if room := maxCaptureBytes - len(c.buf); room > 0 {
+ room := maxCaptureBytes - len(c.buf)
+ if room > 0 {
end := n
if end > room {
end = room
}
c.buf = append(c.buf, p[:end]...)
+ if end < n {
+ c.truncated = true
+ }
+ } else {
+ c.truncated = true
}
c.mu.Unlock()
}
@@ -47,13 +54,19 @@ func (c *teeConn) Read(p []byte) (int, error) {
}
// Take returns the bytes read since the last Take call (or since the
-// connection was created) and resets the buffer.
-func (c *teeConn) Take() []byte {
+// connection was created), whether that capture hit maxCaptureBytes and
+// had to drop bytes off the end, and resets both. A caller storing this
+// as an "exact" capture must fold truncated into that decision - bytes
+// silently missing from the tail is exactly the kind of gap "exact"
+// promises doesn't exist, and callers finding it separately or not at
+// all is how a request smuggling investigation loses the one thing it
+// was looking for.
+func (c *teeConn) Take() (data []byte, truncated bool) {
c.mu.Lock()
defer c.mu.Unlock()
- out := c.buf
- c.buf = nil
- return out
+ out, trunc := c.buf, c.truncated
+ c.buf, c.truncated = nil, false
+ return out, trunc
}
// teeListener wraps a net.Listener so every accepted connection is