Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ When _has add to_ IS set: perform the steps below for each unique participant do
- **add-to set** (adding recipients):
- pid MUST also be set. Fail → respond code 1, close.
- Check if parent stored (§11):
- **Stored**: parent must not be terminal (code 1 if fail); check time travel (code 9 if fail).
- **Stored**: parent must not be terminal (code 1 if fail); check time travel (code 9 if fail); _add to from_ must be in the stored parent's _from_ or _to_ (code 1 if fail). The incoming header's _from_ and _to_ are not evidence of this: the sender wrote them.
- **Not stored**: if ≥ 1 recipient in _to_ or _add to_ belongs to Host B's domain, treat as full message delivery. Otherwise (Host B hosts only non-recipient participants) respond code 6 (parent not found), close.
8. Optionally issue a CHALLENGE on Connection 2 (see §10.5).

Expand Down
35 changes: 35 additions & 0 deletions cmd/fmsgd/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,26 @@ func isMessageRetrievable(msg *FMsgHeader) bool {
return isMessageRetrievable(parentMsg)
}

// isOriginalParticipant reports whether addr is in the stored message's own
// from or to. Only these may add recipients to it (SPEC §12): a recipient added
// by an earlier batch is a participant of that batch, not of the original, and
// a loaded message's AddToFrom/AddTo aren't its own fields anyway.
func isOriginalParticipant(msg *FMsgHeader, addr *FMsgAddress) bool {
if msg == nil || addr == nil {
return false
}
target := strings.ToLower(addr.ToString())
if strings.ToLower(msg.From.ToString()) == target {
return true
}
for i := range msg.To {
if strings.ToLower(msg.To[i].ToString()) == target {
return true
}
}
return false
}

func isParentParticipant(parent *FMsgHeader, addr *FMsgAddress) bool {
if parent == nil || addr == nil {
return false
Expand Down Expand Up @@ -628,6 +648,21 @@ func handleAddToPath(c net.Conn, h *FMsgHeader) (*FMsgHeader, error) {
return h, fmt.Errorf("add-to: time travel detected (parent time %f, current %f)", parentMsg.Timestamp, h.Timestamp)
}

// Only a participant of the stored original may add recipients to it
// (SPEC §10.3 step 7, §12). The header's own from and to can't be
// trusted for this: an outsider could list themselves in both, have the
// original (which they never had) attached to an address they choose.
if !isOriginalParticipant(parentMsg, h.AddToFrom) {
if err := sendCode(c, RejectCodeInvalid); err != nil {
return h, err
}
adder := "(none)"
if h.AddToFrom != nil {
adder = h.AddToFrom.ToString()
}
return h, fmt.Errorf("add-to: add-to-from %s is not in parent msg %d's from or to", adder, parentID)
}

// Batch identity is the batch message hash — this add-to header combined
// with the stored parent's payload (SPEC §11) — so map the parent's data
// in and compute it before the duplicate check.
Expand Down
40 changes: 40 additions & 0 deletions cmd/fmsgd/terminal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,46 @@ func TestHandleAddToPathRejectsTerminalParent(t *testing.T) {
}
}

// Only the stored original's from or to may add recipients: an outsider
// naming themselves in the add-to header's from and to is refused, and so is
// someone who was only added by an earlier batch.
func TestHandleAddToPathRejectsNonParticipant(t *testing.T) {
mallory := FMsgAddress{User: "mallory", Domain: "evil.example"}
parent := storedParentForTest(t, 0)
parent.AddToFrom = &testAlice // a loaded parent carries no batch, but don't rely on it
parent.AddTo = []FMsgAddress{testCarol}
for name, adder := range map[string]FMsgAddress{"outsider": mallory, "added later": testCarol} {
t.Run(name, func(t *testing.T) {
stubStoredParent(t, parent)
h := addToForTest()
adder := adder
h.From, h.To, h.AddToFrom = adder, []FMsgAddress{testBob}, &adder
c := &testConn{}
if _, err := handleAddToPath(c, h); err == nil {
t.Fatal("expected add-to by a non-participant to be refused")
}
if got := c.Bytes(); len(got) != 1 || got[0] != RejectCodeInvalid {
t.Fatalf("wrote %v, want single code %d (invalid)", got, RejectCodeInvalid)
}
})
}
}

// A recipient of the original (in to) may add recipients, as may its sender.
func TestHandleAddToPathAcceptsRecipientAsAdder(t *testing.T) {
stubStoredParent(t, storedParentForTest(t, 0))
h := addToForTest()
bob := testBob
h.AddToFrom = &bob
c := &testConn{}
if _, err := handleAddToPath(c, h); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Len() != 0 {
t.Fatalf("unexpected bytes written: %v", c.Bytes())
}
}

func TestHandleAddToPathAcceptsNonTerminalParent(t *testing.T) {
stubStoredParent(t, storedParentForTest(t, 0))
c := &testConn{}
Expand Down
Loading