SV-33388 -- attachment reached through a surrogate-led new tab

Reproduces the Fujitsu Panorama failure: the user exports a named configuration snapshot, a new tab opens, and it stays a blank white page. No file is downloaded and /safeview-fileserv/ is never contacted.

Preconditions -- read before running

The short version. This test can silently fail to test anything. If the surrogate is not in the right state, every case below passes and it looks like there is no bug. So: set up the four items in the checklist, run the cases, and then prove the bug was armed before you trust any result.

What has to be true, and why

The surrogate sends the client a TAB_open message whose last value is a flag called defer_initial_nav:

You cannot set this flag from a UI. It turns on by itself, in one of two ways (safeview.cc:3212-3217):

  1. the site carries defer_new_tab_nav in its site flags (safeview_site_config.cc:450-452) --- needs a policy change; or
  2. the surrogate process has seen any page with sticky policy (remain_isolate, or a non-empty switch_access such as proxy access). Safeview::SetStickyPolicy (safeview.cc:7602-7622) then latches use_surrogate_led_new_tab() on for the entire surrogate process, for every tab, and never resets it.

Route 2 is what happened in the field capture, which was access=proxy. So the flag is not something you configure --- it is something you verify.

Checklist before you click anything

  1. Web policy for this page's domain is isolate. Not allow, not SSL exception --- otherwise the traffic never reaches the surrogate and there is nothing to reproduce.
  2. Access mode is proxy, matching the capture. Prepend is worth a second pass, but do proxy first.
  3. Browser is Chrome. The customer was on Chrome 150; Safari and IE take a different client path (see case 2).
  4. DevTools open on this tab with Preserve log ticked before the first click. The failing tab generates no requests of its own, so the opener's log is the entire evidence trail.

Then run case 5 first as a smoke test. If a new tab does not open and render "This is basic1 page.", the environment is wrong and nothing else you observe means anything.

Proving the bug was armed

Do this after the run, from the client side --- it needs no surrogate access. In DevTools on this tab: Network, right-click, Save all as HAR with content. Then:

python3 check_defer_flag.py <your-run>.har

It prints every TAB_open with a verdict. TRUE means a blank tab in cases 1-3 is a genuine reproduction; false means the run proved nothing and you need proxy access or the defer_new_tab_nav site flag. What you are looking for in the raw frame is the trailing value:

["TAB_open",<N>,"<.../panorama_config_export.php?name=running-config.xml>",false,"",false,true]
                                                                                        ^^^^
                                                                          defer_initial_nav

If you do have surrogate log access, the equivalent check is the def: field, which the TabOpen serializer prints only when the flag is true (safeview_tc_cmds.cc:173-175):

surrogate-tab-request .* cmd=.*def:

Both rendering modes are affected

The capture contains a DOM run and a pixel run with byte-identical TAB_open payloads. Run every case twice, switching with Ctrl+Alt+Shift+1 (DOM / r1) and Ctrl+Alt+Shift+2 (pixel / ACR2), each of which reloads the page. There is no r1/r2 branch anywhere between Safeview::AddNewContents and the commit gate, so any difference in outcome between the two modes is a finding worth chasing.

1. window.open() inside a click handler -- THE REPORTED CASE

This is what Panorama's export button does, and it matches the capture exactly. The click keeps transient activation, so the surrogate reports is_popup=false, the client takes the no-modal path and answers ["allow-tab-open",true,false,N] --- prompted=false, no popup dialog.

Expected failure: a new tab opens and stays blank white forever. No download, no download modal, no scan. The tab is never released --- it leaks until you close it by hand, at which point the client poller (tab.js:482-499) sends ["tab_close",N]. Between the open and your manual close the WebSocket carries nothing but anim_ping/anim_pong.

2. <a target="_blank"> to the same attachment

running-config-blank.xml (target=_blank)

Same destination, reached by a link click, which goes through tabPreOpen() (tab.js:358, called from local-events.js:1317).

Note what that function actually does: it returns early, opening nothing, whenever window.navigator.userActivation exists. Chrome has it --- including the customer's Chrome 150 --- so on Chrome this case converges onto the very same Util.windowOpen('', '', ...) branch of tabOpenAction as case 1, and is a duplicate. The pre_open_windows.shift() branch is only reached on browsers without userActivation, i.e. Safari and IE. Run this case there if you want coverage of that branch; on Chrome, running it only confirms the two entry points agree.

Expected failure: identical to case 1 --- blank tab, no download. On Safari, a difference from case 1 would mean the pre-open branch matters and the fix has to cover both.

3. window.open() from a timer -- no user activation

The button counts down and then opens the tab. Do not click anywhere while it counts --- any click restarts the activation window and turns this back into case 1.

The wait has to exceed 5 seconds, because that is how long Chrome keeps transient activation alive after a click. An earlier version of this page used setTimeout(..., 0) and a real run showed it still produced is_popup=false --- it was silently a duplicate of case 1. Past the window the activation is genuinely gone, the surrogate reports is_popup=true, and the client shows the popup-open modal, answering ["allow-tab-open",true,true,N] with prompted=true. Check the frame if in doubt: the 3rd value is is_popup and it must be true for this case to be doing its job.

Expected failure: popup modal, then --- after you allow it --- the same blank tab. Included to keep the two routes apart: this is not the reported case (the capture shows prompted=false on all three attempts), so do not use it to judge a fix for the reported case. It should be fixed by the same change, but a fix that only repairs this path has not repaired the customer's.

4. Control -- same attachment, same tab

Expected pass: the download modal appears, the file is scanned, and running-config-same-tab.xml arrives. This proves the PHP response, the download pipeline and /safeview-fileserv/ are all healthy --- the attachment is not the problem on its own. Navigating this tab away also fires letChildrenGo() (history.js:349), so run it in a fresh page load rather than after case 1 or 2, or it will release their orphan tab and muddy the result.

5. Control -- surrogate-led new tab to a normal page

Expected pass: the new tab pairs and renders "This is basic1 page." Same TAB_open with defer_initial_nav=true, but the navigation commits, so SetClientLocation runs and client_loc with action:"initial" is sent to the opener, which drives the child tab. This proves deferred new-tab navigation itself works.

Cases 4 and 5 bracket the defect: download without a new tab passes, new tab without a download passes, and only case 1 --- both at once --- fails.

What to capture on a run

Client side, in the WebSocket frames for the opener tab, the whole failure signature is four messages and a gap:

S->C  ["TAB_open",N,"<export url>",false,"",false,true]
C->S  ["allow-tab-open",true,false,N]
C->S  ["hide-tab"]
      ... only anim_ping / anim_pong ...
C->S  ["tab_close",N]                       (only when you close it by hand)

Plus a negative check: zero requests to /safeview-fileserv/, and no WebSocket ever opening with tab_id=N.

Surrogate side, in order --- the last line is the one that must be absent:

surrogate-tab-request .* def:                  # deferred open, new tab N
surrogate-allow-tab-open .* new_tab=N
surrogate-policy-check-result                  # PE ran for the export URL
surrogate-nav-not-committed .* is_download=t   # tab=N, the gate closes here
surrogate-download-requested                   # tab=N (the orphan), not the opener
surrogate-nav-bar-update                       # ABSENT for tab=N
surrogate-lost-navigator                       # ABSENT (distinguishes this from
                                               #   an opener-closed-early race)