mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
A folio login run wrote the account email and password in cleartext into llm-calls.jsonl, 166 times in one run, beside screenshots of the same screens. Three sites rendered it: the recent-action memory, the candidate list, and the trace. One helper now covers all three so a fourth cannot bypass it, and the driver still receives the real text. Android redacts every typed value because it cannot tell a secure field from any other. That asymmetry is deliberate and documented: safe by default on the target that cannot tell.
72 lines
2.7 KiB
Go
72 lines
2.7 KiB
Go
package verifier
|
|
|
|
import (
|
|
"github.com/dop251/goja"
|
|
|
|
"github.com/priyanshujain/sanderling/internal/hierarchy"
|
|
)
|
|
|
|
// RedactedInputText stands in for a typed value in every record. It is fixed,
|
|
// so it gives away neither the value nor its length.
|
|
const RedactedInputText = "[redacted]"
|
|
|
|
// secureFact is what a target says about being a secure text entry. `reported`
|
|
// separates "the platform says this is not one" from "the platform says
|
|
// nothing", which are the two cases the redaction rule has to tell apart.
|
|
type secureFact struct {
|
|
reported bool
|
|
secure bool
|
|
}
|
|
|
|
// secureFactFromHandle reads an element handle's own report. The web host
|
|
// injects handles built in the page, which carry no selector to resolve against
|
|
// the goja-side tree, so the handle is the only thing that knows.
|
|
func secureFactFromHandle(object *goja.Object) secureFact {
|
|
value := object.Get("secure")
|
|
if value == nil || goja.IsUndefined(value) || goja.IsNull(value) {
|
|
return secureFact{}
|
|
}
|
|
return secureFact{reported: true, secure: value.ToBoolean()}
|
|
}
|
|
|
|
func secureFactOf(element *hierarchy.Element) secureFact {
|
|
if element == nil {
|
|
return secureFact{}
|
|
}
|
|
return secureFact{reported: element.SecureReported(), secure: element.Secure}
|
|
}
|
|
|
|
// RecordedActionText renders the typed value of an action for anything that is
|
|
// persisted or sent, resolving the action's target in the tree it was chosen
|
|
// against.
|
|
func RecordedActionText(action Action, tree *hierarchy.Tree) string {
|
|
if action.Kind != ActionKindInputText {
|
|
return action.Text
|
|
}
|
|
var target *hierarchy.Element
|
|
if tree != nil && action.On != "" {
|
|
target = tree.Find(action.On)
|
|
}
|
|
return recordedInputText(action.Text, secureFactOf(target))
|
|
}
|
|
|
|
// recordedInputText is the one place a typed value is rendered for a record.
|
|
// The prompt's recent-action memory, the numbered candidate list and the trace
|
|
// all go through it, so a fourth record added later cannot publish a value the
|
|
// other three withhold. The driver dispatch reads Action.Text directly and is
|
|
// the only reader of the real value, which is what keeps the app receiving the
|
|
// keystrokes a user would have produced.
|
|
//
|
|
// A target the platform reports as a secure entry is redacted, and so is a
|
|
// target carrying no report at all. iOS and web state the fact on every
|
|
// editable element, so a missing one means Android, whose native tree mapper
|
|
// drops uiautomator's password attribute before the sidecar sees it. There a
|
|
// password field cannot be told from a search box, and the target that cannot
|
|
// be told apart is treated as the credential.
|
|
func recordedInputText(text string, target secureFact) string {
|
|
if target.reported && !target.secure {
|
|
return text
|
|
}
|
|
return RedactedInputText
|
|
}
|