@@ -204,6 +204,72 @@ func TestSelectLoginFlow(t *testing.T) {
204204 })
205205}
206206
207+ func TestValidateBrowserURL (t * testing.T ) {
208+ tests := []struct {
209+ name string
210+ url string
211+ wantErr bool
212+ }{
213+ {name : "valid https" , url : "https://example.com/device?code=ABC" , wantErr : false },
214+ {name : "valid http" , url : "http://localhost:3080/auth" , wantErr : false },
215+ {name : "command injection ampersand" , url : "https://example.com & calc.exe" , wantErr : true },
216+ {name : "command injection pipe" , url : "https://x | powershell -enc ZABp" , wantErr : true },
217+ {name : "file scheme" , url : "file:///etc/passwd" , wantErr : true },
218+ {name : "javascript scheme" , url : "javascript:alert(1)" , wantErr : true },
219+ {name : "empty scheme" , url : "://no-scheme" , wantErr : true },
220+ {name : "no host" , url : "https://" , wantErr : true },
221+ {name : "relative path" , url : "/just/a/path" , wantErr : true },
222+ }
223+ for _ , tt := range tests {
224+ t .Run (tt .name , func (t * testing.T ) {
225+ err := validateBrowserURL (tt .url )
226+ if (err != nil ) != tt .wantErr {
227+ t .Errorf ("validateBrowserURL(%q) error = %v, wantErr %v" , tt .url , err , tt .wantErr )
228+ }
229+ })
230+ }
231+ }
232+
233+ // TestValidateBrowserURL_WindowsRundll32Escape tests that validateBrowserURL blocks
234+ // payloads that could abuse the Windows "rundll32 url.dll,OpenURL" browser opener
235+ // (LOLBAS T1218.011). If any of these cases pass validation, an attacker-controlled
236+ // URL could execute arbitrary files via rundll32.
237+ // Reference: https://lolbas-project.github.io/lolbas/Libraries/Url/
238+ func TestValidateBrowserURL_WindowsRundll32Escape (t * testing.T ) {
239+ tests := []struct {
240+ name string
241+ url string
242+ }{
243+ // url.dll OpenURL can launch .hta payloads via mshta.exe
244+ {name : "hta via file protocol" , url : "file:///C:/Temp/payload.hta" },
245+ // url.dll OpenURL can launch executables from .url shortcut files
246+ {name : "url shortcut file" , url : "file:///C:/Temp/launcher.url" },
247+ // url.dll OpenURL / FileProtocolHandler can run executables directly
248+ {name : "exe via file protocol" , url : "file:///C:/Windows/System32/calc.exe" },
249+ // Obfuscated file protocol handler variant
250+ {name : "obfuscated file handler" , url : "file:///C:/Temp/payload.exe" },
251+ // UNC path via file scheme to remote payload
252+ {name : "unc path file scheme" , url : "file://attacker.com/share/payload.exe" },
253+ // data: URI could be passed through to a handler
254+ {name : "data uri" , url : "data:text/html,<script>alert(1)</script>" },
255+ // vbscript scheme
256+ {name : "vbscript scheme" , url : "vbscript:Execute(\" MsgBox(1)\" )" },
257+ // about scheme
258+ {name : "about scheme" , url : "about:blank" },
259+ // ms-msdt protocol handler (Follina-style)
260+ {name : "ms-msdt handler" , url : "ms-msdt:/id PCWDiagnostic /skip force /param" },
261+ // search-ms protocol handler
262+ {name : "search-ms handler" , url : "search-ms:query=calc&crumb=location:\\ \\ attacker.com\\ share" },
263+ }
264+ for _ , tt := range tests {
265+ t .Run (tt .name , func (t * testing.T ) {
266+ if err := validateBrowserURL (tt .url ); err == nil {
267+ t .Errorf ("validateBrowserURL(%q) = nil; want error (payload must be blocked to prevent rundll32 url.dll,OpenURL abuse)" , tt .url )
268+ }
269+ })
270+ }
271+ }
272+
207273func restoreStoredOAuthLoader (t * testing.T , loader func (context.Context , string ) (* oauth.Token , error )) {
208274 t .Helper ()
209275
0 commit comments