detached 1ee1a70

fix(webview): harden reviewed bridge paths (#372)

commit
1ee1a70f5f47523cf6bc8b19905cfe58cbf455d3
author
bnema · · 2026-08-13 08:14 UTC
committer
GitHub · · 2026-08-13 08:14 UTC
parent
f477efa
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
3 files · +8 -5
M internal/infrastructure/webkit/messaging.go +4 -3
@@ -282,7 +282,7 @@ func decodeScriptMessage(valuePtr uintptr, log zerolog.Logger) (Message, bool) {
282282
283283 var msg Message
284284 if err := json.Unmarshal([]byte(rawJSON), &msg); err != nil {
285- log.Warn().Err(err).Str("json", rawJSON).Msg("failed to unmarshal script message")
285+ log.Warn().Err(err).Int("json_len", len(rawJSON)).Msg("failed to unmarshal script message")
286286 return Message{}, false
287287 }
288288 return msg, true
@@ -305,14 +305,15 @@ func (r *MessageRouter) handleAllowlistedBridgeMessage(senderWV *WebView, msg Me
305305 Editable bool `json:"editable"`
306306 Token string `json:"token"`
307307 }
308+ log := logging.FromContext(r.baseContext())
308309 if len(msg.Payload) != 0 {
309310 if err := json.Unmarshal(msg.Payload, &payload); err != nil {
310- logging.FromContext(r.baseCtx).Warn().Err(err).Str("type", msg.Type).Msg("failed to decode allowlisted bridge payload")
311+ log.Warn().Err(err).Str("type", msg.Type).Msg("failed to decode allowlisted bridge payload")
311312 return true
312313 }
313314 }
314315 if payload.Token == "" || !senderWV.matchesEditableFocusBridgeToken(payload.Token) {
315- logging.FromContext(r.baseCtx).Warn().Str("type", msg.Type).Msg("rejecting allowlisted bridge message with invalid token")
316+ log.Warn().Str("type", msg.Type).Msg("rejecting allowlisted bridge message with invalid token")
316317 return true
317318 }
318319 senderWV.dispatchEditableFocusChanged(payload.Editable)
@@ -282,7 +282,7 @@ func decodeScriptMessage(valuePtr uintptr, log zerolog.Logger) (Message, bool) {
282 282
283 var msg Message283 var msg Message
284 if err := json.Unmarshal([]byte(rawJSON), &msg); err != nil {284 if err := json.Unmarshal([]byte(rawJSON), &msg); err != nil {
285- log.Warn().Err(err).Str("json", rawJSON).Msg("failed to unmarshal script message")285+ log.Warn().Err(err).Int("json_len", len(rawJSON)).Msg("failed to unmarshal script message")
286 return Message{}, false286 return Message{}, false
287 }287 }
288 return msg, true288 return msg, true
@@ -305,14 +305,15 @@ func (r *MessageRouter) handleAllowlistedBridgeMessage(senderWV *WebView, msg Me
305 Editable bool `json:"editable"`305 Editable bool `json:"editable"`
306 Token string `json:"token"`306 Token string `json:"token"`
307 }307 }
308+ log := logging.FromContext(r.baseContext())
308 if len(msg.Payload) != 0 {309 if len(msg.Payload) != 0 {
309 if err := json.Unmarshal(msg.Payload, &payload); err != nil {310 if err := json.Unmarshal(msg.Payload, &payload); err != nil {
310- logging.FromContext(r.baseCtx).Warn().Err(err).Str("type", msg.Type).Msg("failed to decode allowlisted bridge payload")311+ log.Warn().Err(err).Str("type", msg.Type).Msg("failed to decode allowlisted bridge payload")
311 return true312 return true
312 }313 }
313 }314 }
314 if payload.Token == "" || !senderWV.matchesEditableFocusBridgeToken(payload.Token) {315 if payload.Token == "" || !senderWV.matchesEditableFocusBridgeToken(payload.Token) {
315- logging.FromContext(r.baseCtx).Warn().Str("type", msg.Type).Msg("rejecting allowlisted bridge message with invalid token")316+ log.Warn().Str("type", msg.Type).Msg("rejecting allowlisted bridge message with invalid token")
316 return true317 return true
317 }318 }
318 senderWV.dispatchEditableFocusChanged(payload.Editable)319 senderWV.dispatchEditableFocusChanged(payload.Editable)
M internal/infrastructure/webkit/pool.go +3 -1
@@ -179,7 +179,9 @@ func (p *WebViewPool) Acquire(ctx context.Context) (*WebView, error) {
179179
180180 // Ensure frontend is attached even if this WebView was pooled before injection was configured.
181181 alreadyAttached := wv.FrontendAttached()
182- _ = wv.AttachFrontend(ctx, p.injector, p.router)
182+ if err := wv.AttachFrontend(ctx, p.injector, p.router); err != nil {
183+ log.Warn().Err(err).Uint64("id", uint64(wv.ID())).Msg("failed to attach frontend to pooled webview")
184+ }
183185 // Reinjection is only for already-attached reuse after ResetForPoolReuse.
184186 // First AttachFrontend already injected; avoid an immediate double inject.
185187 if needsFrontendReinjectionOnAcquire(alreadyAttached) && p.injector != nil && wv.ucm != nil {
@@ -179,7 +179,9 @@ func (p *WebViewPool) Acquire(ctx context.Context) (*WebView, error) {
179 179
180 // Ensure frontend is attached even if this WebView was pooled before injection was configured.180 // Ensure frontend is attached even if this WebView was pooled before injection was configured.
181 alreadyAttached := wv.FrontendAttached()181 alreadyAttached := wv.FrontendAttached()
182- _ = wv.AttachFrontend(ctx, p.injector, p.router)182+ if err := wv.AttachFrontend(ctx, p.injector, p.router); err != nil {
183+ log.Warn().Err(err).Uint64("id", uint64(wv.ID())).Msg("failed to attach frontend to pooled webview")
184+ }
183 // Reinjection is only for already-attached reuse after ResetForPoolReuse.185 // Reinjection is only for already-attached reuse after ResetForPoolReuse.
184 // First AttachFrontend already injected; avoid an immediate double inject.186 // First AttachFrontend already injected; avoid an immediate double inject.
185 if needsFrontendReinjectionOnAcquire(alreadyAttached) && p.injector != nil && wv.ucm != nil {187 if needsFrontendReinjectionOnAcquire(alreadyAttached) && p.injector != nil && wv.ucm != nil {
M internal/ui/app_browser_launch_test.go +1 -1
@@ -1009,7 +1009,7 @@ func TestTestShellToasterCleanupCancelsAutoDismiss(t *testing.T) {
10091009 toaster.CancelAutoDismiss()
10101010
10111011 mainContext := glib.MainContextDefault()
1012- deadline := time.Now().Add(component.ToastBriefDurationMs + 100*time.Millisecond)
1012+ deadline := time.Now().Add(component.ToastBriefDurationMs + 600*time.Millisecond)
10131013 for time.Now().Before(deadline) {
10141014 for mainContext.Pending() {
10151015 mainContext.Iteration(false)
@@ -1009,7 +1009,7 @@ func TestTestShellToasterCleanupCancelsAutoDismiss(t *testing.T) {
1009 toaster.CancelAutoDismiss()1009 toaster.CancelAutoDismiss()
1010 1010
1011 mainContext := glib.MainContextDefault()1011 mainContext := glib.MainContextDefault()
1012- deadline := time.Now().Add(component.ToastBriefDurationMs + 100*time.Millisecond)1012+ deadline := time.Now().Add(component.ToastBriefDurationMs + 600*time.Millisecond)
1013 for time.Now().Before(deadline) {1013 for time.Now().Before(deadline) {
1014 for mainContext.Pending() {1014 for mainContext.Pending() {
1015 mainContext.Iteration(false)1015 mainContext.Iteration(false)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close