github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/headless/engine/page_actions_test.go (about)

     1  package engine
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"math/rand"
     7  	"net/http"
     8  	"net/http/cookiejar"
     9  	"net/http/httptest"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/stretchr/testify/require"
    19  
    20  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
    21  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolstate"
    22  	"github.com/projectdiscovery/nuclei/v2/pkg/testutils/testheadless"
    23  	"github.com/projectdiscovery/nuclei/v2/pkg/types"
    24  	stringsutil "github.com/projectdiscovery/utils/strings"
    25  )
    26  
    27  func TestActionNavigate(t *testing.T) {
    28  	response := `
    29  		<html>
    30  		<head>
    31  			<title>Nuclei Test Page</title>
    32  		</head>
    33  		<body>
    34  			<h1>Nuclei Test</h1>
    35  		</body>
    36  	</html>`
    37  
    38  	actions := []*Action{{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}}, {ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}}}
    39  
    40  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
    41  		require.Nilf(t, err, "could not run page actions")
    42  		require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
    43  	})
    44  }
    45  
    46  func TestActionScript(t *testing.T) {
    47  	response := `
    48  		<html>
    49  		<head>
    50  			<title>Nuclei Test Page</title>
    51  		</head>
    52  		<body>Nuclei Test Page</body>
    53  		<script>window.test = 'some-data';</script>
    54  	</html>`
    55  
    56  	timeout := 180 * time.Second
    57  
    58  	t.Run("run-and-results", func(t *testing.T) {
    59  		actions := []*Action{
    60  			{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
    61  			{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
    62  			{ActionType: ActionTypeHolder{ActionType: ActionScript}, Name: "test", Data: map[string]string{"code": "() => window.test"}},
    63  		}
    64  
    65  		testHeadlessSimpleResponse(t, response, actions, timeout, func(page *Page, err error, out map[string]string) {
    66  			require.Nil(t, err, "could not run page actions")
    67  			require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
    68  			require.Equal(t, "some-data", out["test"], "could not run js and get results correctly")
    69  		})
    70  	})
    71  
    72  	t.Run("hook", func(t *testing.T) {
    73  		actions := []*Action{
    74  			{ActionType: ActionTypeHolder{ActionType: ActionScript}, Data: map[string]string{"code": "() => window.test = 'some-data';", "hook": "true"}},
    75  			{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
    76  			{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
    77  			{ActionType: ActionTypeHolder{ActionType: ActionScript}, Name: "test", Data: map[string]string{"code": "() => window.test"}},
    78  		}
    79  		testHeadlessSimpleResponse(t, response, actions, timeout, func(page *Page, err error, out map[string]string) {
    80  			require.Nil(t, err, "could not run page actions")
    81  			require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
    82  			require.Equal(t, "some-data", out["test"], "could not run js and get results correctly with js hook")
    83  		})
    84  	})
    85  }
    86  
    87  func TestActionClick(t *testing.T) {
    88  	response := `
    89  		<html>
    90  			<head>
    91  				<title>Nuclei Test Page</title>
    92  			</head>
    93  			<body>Nuclei Test Page</body>
    94  			<button onclick='this.setAttribute("a", "ok")'>click me</button>
    95  		</html>`
    96  
    97  	actions := []*Action{
    98  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
    99  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   100  		{ActionType: ActionTypeHolder{ActionType: ActionClick}, Data: map[string]string{"selector": "button"}}, // Use css selector for clicking
   101  	}
   102  
   103  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   104  		require.Nil(t, err, "could not run page actions")
   105  		require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
   106  		el := page.Page().MustElement("button")
   107  		val := el.MustAttribute("a")
   108  		require.Equal(t, "ok", *val, "could not click button")
   109  	})
   110  }
   111  
   112  func TestActionRightClick(t *testing.T) {
   113  	response := `
   114  		<html>
   115  			<head>
   116  				<title>Nuclei Test Page</title>
   117  			</head>
   118  			<body>Nuclei Test Page</body>
   119  			<button id="test" onrightclick=''>click me</button>
   120  			<script>
   121  				elm = document.getElementById("test");
   122  				elm.onmousedown = function(event) {
   123  					if (event.which == 3) {
   124  						elm.setAttribute("a", "ok")
   125  					}
   126  				}
   127  			</script>
   128  		</html>`
   129  
   130  	actions := []*Action{
   131  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   132  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   133  		{ActionType: ActionTypeHolder{ActionType: ActionRightClick}, Data: map[string]string{"selector": "button"}}, // Use css selector for clicking
   134  	}
   135  
   136  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   137  		require.Nil(t, err, "could not run page actions")
   138  		require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
   139  		el := page.Page().MustElement("button")
   140  		val := el.MustAttribute("a")
   141  		require.Equal(t, "ok", *val, "could not click button")
   142  	})
   143  }
   144  
   145  func TestActionTextInput(t *testing.T) {
   146  	response := `
   147  		<html>
   148  			<head>
   149  				<title>Nuclei Test Page</title>
   150  			</head>
   151  			<body>Nuclei Test Page</body>
   152  			<input type="text" onchange="this.setAttribute('event', 'input-change')">
   153  		</html>`
   154  
   155  	actions := []*Action{
   156  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   157  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   158  		{ActionType: ActionTypeHolder{ActionType: ActionTextInput}, Data: map[string]string{"selector": "input", "value": "test"}},
   159  	}
   160  
   161  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   162  		require.Nil(t, err, "could not run page actions")
   163  		require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
   164  		el := page.Page().MustElement("input")
   165  		val := el.MustAttribute("event")
   166  		require.Equal(t, "input-change", *val, "could not get input change")
   167  		require.Equal(t, "test", el.MustText(), "could not get input change value")
   168  	})
   169  }
   170  
   171  func TestActionHeadersChange(t *testing.T) {
   172  	actions := []*Action{
   173  		{ActionType: ActionTypeHolder{ActionType: ActionSetHeader}, Data: map[string]string{"part": "request", "key": "Test", "value": "Hello"}},
   174  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   175  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   176  	}
   177  
   178  	handler := func(w http.ResponseWriter, r *http.Request) {
   179  		if r.Header.Get("Test") == "Hello" {
   180  			_, _ = fmt.Fprintln(w, `found`)
   181  		}
   182  	}
   183  
   184  	testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out map[string]string) {
   185  		require.Nil(t, err, "could not run page actions")
   186  		require.Equal(t, "found", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not set header correctly")
   187  	})
   188  }
   189  
   190  func TestActionScreenshot(t *testing.T) {
   191  	response := `
   192  		<html>
   193  			<head>
   194  				<title>Nuclei Test Page</title>
   195  			</head>
   196  			<body>Nuclei Test Page</body>
   197  		</html>`
   198  
   199  	// filePath where screenshot is saved
   200  	filePath := filepath.Join(os.TempDir(), "test.png")
   201  	actions := []*Action{
   202  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   203  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   204  		{ActionType: ActionTypeHolder{ActionType: ActionScreenshot}, Data: map[string]string{"to": filePath}},
   205  	}
   206  
   207  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   208  		require.Nil(t, err, "could not run page actions")
   209  		require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
   210  		_ = page.Page()
   211  		require.FileExists(t, filePath, "could not find screenshot file %v", filePath)
   212  		if err := os.RemoveAll(filePath); err != nil {
   213  			t.Logf("got error %v while deleting temp file", err)
   214  		}
   215  	})
   216  }
   217  
   218  func TestActionScreenshotToDir(t *testing.T) {
   219  	response := `
   220  		<html>
   221  			<head>
   222  				<title>Nuclei Test Page</title>
   223  			</head>
   224  			<body>Nuclei Test Page</body>
   225  		</html>`
   226  
   227  	filePath := filepath.Join(os.TempDir(), "screenshot-"+strconv.Itoa(rand.Intn(1000)), "test.png")
   228  
   229  	actions := []*Action{
   230  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   231  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   232  		{ActionType: ActionTypeHolder{ActionType: ActionScreenshot}, Data: map[string]string{"to": filePath, "mkdir": "true"}},
   233  	}
   234  
   235  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   236  		require.Nil(t, err, "could not run page actions")
   237  		require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
   238  		_ = page.Page()
   239  		require.FileExists(t, filePath, "could not find screenshot file %v", filePath)
   240  		if err := os.RemoveAll(filePath); err != nil {
   241  			t.Logf("got error %v while deleting temp file", err)
   242  		}
   243  	})
   244  }
   245  
   246  func TestActionTimeInput(t *testing.T) {
   247  	response := `
   248  		<html>
   249  			<head>
   250  				<title>Nuclei Test Page</title>
   251  			</head>
   252  			<body>Nuclei Test Page</body>
   253  			<input type="date">
   254  		</html>`
   255  
   256  	actions := []*Action{
   257  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   258  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   259  		{ActionType: ActionTypeHolder{ActionType: ActionTimeInput}, Data: map[string]string{"selector": "input", "value": "2006-01-02T15:04:05Z"}},
   260  	}
   261  
   262  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   263  		require.Nil(t, err, "could not run page actions")
   264  		require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
   265  		el := page.Page().MustElement("input")
   266  		require.Equal(t, "2006-01-02", el.MustText(), "could not get input time value")
   267  	})
   268  }
   269  
   270  func TestActionSelectInput(t *testing.T) {
   271  	response := `
   272  		<html>
   273  			<head>
   274  				<title>Nuclei Test Page</title>
   275  			</head>
   276  			<body>
   277  				<select name="test" id="test">
   278  				  <option value="test1">Test1</option>
   279  				  <option value="test2">Test2</option>
   280  				</select>
   281  			</body>
   282  		</html>`
   283  
   284  	actions := []*Action{
   285  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   286  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   287  		{ActionType: ActionTypeHolder{ActionType: ActionSelectInput}, Data: map[string]string{"by": "x", "xpath": "//select[@id='test']", "value": "Test2", "selected": "true"}},
   288  	}
   289  
   290  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   291  		require.Nil(t, err, "could not run page actions")
   292  		el := page.Page().MustElement("select")
   293  		require.Equal(t, "Test2", el.MustText(), "could not get input change value")
   294  	})
   295  }
   296  
   297  func TestActionFilesInput(t *testing.T) {
   298  	response := `
   299  		<html>
   300  			<head>
   301  				<title>Nuclei Test Page</title>
   302  			</head>
   303  			<body>Nuclei Test Page</body>
   304  			<input type="file">
   305  		</html>`
   306  
   307  	actions := []*Action{
   308  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   309  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   310  		{ActionType: ActionTypeHolder{ActionType: ActionFilesInput}, Data: map[string]string{"selector": "input", "value": "test1.pdf"}},
   311  	}
   312  
   313  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   314  		require.Nil(t, err, "could not run page actions")
   315  		require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
   316  		el := page.Page().MustElement("input")
   317  		require.Equal(t, "C:\\fakepath\\test1.pdf", el.MustText(), "could not get input file")
   318  	})
   319  }
   320  
   321  // Negative testcase for files input where it should fail
   322  func TestActionFilesInputNegative(t *testing.T) {
   323  	response := `
   324  		<html>
   325  			<head>
   326  				<title>Nuclei Test Page</title>
   327  			</head>
   328  			<body>Nuclei Test Page</body>
   329  			<input type="file">
   330  		</html>`
   331  
   332  	actions := []*Action{
   333  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   334  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   335  		{ActionType: ActionTypeHolder{ActionType: ActionFilesInput}, Data: map[string]string{"selector": "input", "value": "test1.pdf"}},
   336  	}
   337  	t.Setenv("LOCAL_FILE_ACCESS", "false")
   338  
   339  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   340  		require.ErrorContains(t, err, ErrLFAccessDenied.Error(), "got file access when -lfa is false")
   341  	})
   342  }
   343  
   344  func TestActionWaitLoad(t *testing.T) {
   345  	response := `
   346  		<html>
   347  			<head>
   348  				<title>Nuclei Test Page</title>
   349  			</head>
   350  			<button id="test">Wait for me!</button>
   351  			<script>
   352  				window.onload = () => document.querySelector('#test').style.color = 'red';
   353  			</script>
   354  		</html>`
   355  
   356  	actions := []*Action{
   357  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   358  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   359  	}
   360  
   361  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   362  		require.Nil(t, err, "could not run page actions")
   363  		el := page.Page().MustElement("button")
   364  		style, attributeErr := el.Attribute("style")
   365  		require.Nil(t, attributeErr)
   366  		require.Equal(t, "color: red;", *style, "could not get color")
   367  	})
   368  }
   369  
   370  func TestActionGetResource(t *testing.T) {
   371  	response := `
   372  		<html>
   373  			<head>
   374  				<title>Nuclei Test Page</title>
   375  			</head>
   376  			<body>
   377  				<img id="test" src="https://raw.githubusercontent.com/projectdiscovery/wallpapers/main/pd-floppy.jpg">
   378  			</body>
   379  		</html>`
   380  
   381  	actions := []*Action{
   382  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   383  		{ActionType: ActionTypeHolder{ActionType: ActionGetResource}, Data: map[string]string{"by": "x", "xpath": "//img[@id='test']"}, Name: "src"},
   384  	}
   385  
   386  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   387  		require.Nil(t, err, "could not run page actions")
   388  		require.Equal(t, len(out["src"]), 121808, "could not find resource")
   389  	})
   390  }
   391  
   392  func TestActionExtract(t *testing.T) {
   393  	response := `
   394  		<html>
   395  			<head>
   396  				<title>Nuclei Test Page</title>
   397  			</head>
   398  			<button id="test">Wait for me!</button>
   399  		</html>`
   400  
   401  	actions := []*Action{
   402  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   403  		{ActionType: ActionTypeHolder{ActionType: ActionExtract}, Data: map[string]string{"by": "x", "xpath": "//button[@id='test']"}, Name: "extract"},
   404  	}
   405  
   406  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   407  		require.Nil(t, err, "could not run page actions")
   408  		require.Equal(t, "Wait for me!", out["extract"], "could not extract text")
   409  	})
   410  }
   411  
   412  func TestActionSetMethod(t *testing.T) {
   413  	response := `
   414  		<html>
   415  			<head>
   416  				<title>Nuclei Test Page</title>
   417  			</head>
   418  		</html>`
   419  
   420  	actions := []*Action{
   421  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   422  		{ActionType: ActionTypeHolder{ActionType: ActionSetMethod}, Data: map[string]string{"part": "x", "method": "SET"}},
   423  	}
   424  
   425  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   426  		require.Nil(t, err, "could not run page actions")
   427  		require.Equal(t, "SET", page.rules[0].Args["method"], "could not find resource")
   428  	})
   429  }
   430  
   431  func TestActionAddHeader(t *testing.T) {
   432  	actions := []*Action{
   433  		{ActionType: ActionTypeHolder{ActionType: ActionAddHeader}, Data: map[string]string{"part": "request", "key": "Test", "value": "Hello"}},
   434  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   435  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   436  	}
   437  
   438  	handler := func(w http.ResponseWriter, r *http.Request) {
   439  		if r.Header.Get("Test") == "Hello" {
   440  			_, _ = fmt.Fprintln(w, `found`)
   441  		}
   442  	}
   443  
   444  	testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out map[string]string) {
   445  		require.Nil(t, err, "could not run page actions")
   446  		require.Equal(t, "found", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not set header correctly")
   447  	})
   448  }
   449  
   450  func TestActionDeleteHeader(t *testing.T) {
   451  	actions := []*Action{
   452  		{ActionType: ActionTypeHolder{ActionType: ActionAddHeader}, Data: map[string]string{"part": "request", "key": "Test1", "value": "Hello"}},
   453  		{ActionType: ActionTypeHolder{ActionType: ActionAddHeader}, Data: map[string]string{"part": "request", "key": "Test2", "value": "World"}},
   454  		{ActionType: ActionTypeHolder{ActionType: ActionDeleteHeader}, Data: map[string]string{"part": "request", "key": "Test2"}},
   455  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   456  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   457  	}
   458  
   459  	handler := func(w http.ResponseWriter, r *http.Request) {
   460  		if r.Header.Get("Test1") == "Hello" && r.Header.Get("Test2") == "" {
   461  			_, _ = fmt.Fprintln(w, `header deleted`)
   462  		}
   463  	}
   464  
   465  	testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out map[string]string) {
   466  		require.Nil(t, err, "could not run page actions")
   467  		require.Equal(t, "header deleted", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not delete header correctly")
   468  	})
   469  }
   470  
   471  func TestActionSetBody(t *testing.T) {
   472  	actions := []*Action{
   473  		{ActionType: ActionTypeHolder{ActionType: ActionSetBody}, Data: map[string]string{"part": "request", "body": "hello"}},
   474  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   475  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   476  	}
   477  
   478  	handler := func(w http.ResponseWriter, r *http.Request) {
   479  		body, _ := io.ReadAll(r.Body)
   480  		_, _ = fmt.Fprintln(w, string(body))
   481  	}
   482  
   483  	testHeadless(t, actions, 20*time.Second, handler, func(page *Page, err error, out map[string]string) {
   484  		require.Nil(t, err, "could not run page actions")
   485  		require.Equal(t, "hello", strings.ToLower(strings.TrimSpace(page.Page().MustElement("html").MustText())), "could not set header correctly")
   486  	})
   487  }
   488  
   489  func TestActionKeyboard(t *testing.T) {
   490  	response := `
   491  		<html>
   492  			<head>
   493  				<title>Nuclei Test Page</title>
   494  			</head>
   495  			<body>
   496  				<input type="text" name="test" id="test">
   497  			</body>
   498  		</html>`
   499  
   500  	actions := []*Action{
   501  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   502  		{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   503  		{ActionType: ActionTypeHolder{ActionType: ActionClick}, Data: map[string]string{"selector": "input"}},
   504  		{ActionType: ActionTypeHolder{ActionType: ActionKeyboard}, Data: map[string]string{"keys": "Test2"}},
   505  	}
   506  
   507  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   508  		require.Nil(t, err, "could not run page actions")
   509  		el := page.Page().MustElement("input")
   510  		require.Equal(t, "Test2", el.MustText(), "could not get input change value")
   511  	})
   512  }
   513  
   514  func TestActionSleep(t *testing.T) {
   515  	response := `
   516  		<html>
   517  			<head>
   518  				<title>Nuclei Test Page</title>
   519  			</head>
   520  			<button style="display:none" id="test">Wait for me!</button>
   521  			<script>
   522  				setTimeout(() => document.querySelector('#test').style.display = '', 1000);
   523  			</script>
   524  		</html>`
   525  
   526  	actions := []*Action{
   527  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   528  		{ActionType: ActionTypeHolder{ActionType: ActionSleep}, Data: map[string]string{"duration": "2"}},
   529  	}
   530  
   531  	testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out map[string]string) {
   532  		require.Nil(t, err, "could not run page actions")
   533  		require.True(t, page.Page().MustElement("button").MustVisible(), "could not get button")
   534  	})
   535  }
   536  
   537  func TestActionWaitVisible(t *testing.T) {
   538  	response := `
   539  		<html>
   540  			<head>
   541  				<title>Nuclei Test Page</title>
   542  			</head>
   543  			<button style="display:none" id="test">Wait for me!</button>
   544  			<script>
   545  				setTimeout(() => document.querySelector('#test').style.display = '', 1000);
   546  			</script>
   547  		</html>`
   548  
   549  	actions := []*Action{
   550  		{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}},
   551  		{ActionType: ActionTypeHolder{ActionType: ActionWaitVisible}, Data: map[string]string{"by": "x", "xpath": "//button[@id='test']"}},
   552  	}
   553  
   554  	t.Run("wait for an element being visible", func(t *testing.T) {
   555  		testHeadlessSimpleResponse(t, response, actions, 2*time.Second, func(page *Page, err error, out map[string]string) {
   556  			require.Nil(t, err, "could not run page actions")
   557  
   558  			page.Page().MustElement("button").MustVisible()
   559  		})
   560  	})
   561  
   562  	t.Run("timeout because of element not visible", func(t *testing.T) {
   563  		// increased timeout from time.Second/2 to time.Second due to random fails (probably due to overhead and system)
   564  		testHeadlessSimpleResponse(t, response, actions, time.Second, func(page *Page, err error, out map[string]string) {
   565  			require.Error(t, err)
   566  			require.Contains(t, err.Error(), "Element did not appear in the given amount of time")
   567  		})
   568  	})
   569  }
   570  
   571  func testHeadlessSimpleResponse(t *testing.T, response string, actions []*Action, timeout time.Duration, assert func(page *Page, pageErr error, out map[string]string)) {
   572  	t.Helper()
   573  	testHeadless(t, actions, timeout, func(w http.ResponseWriter, r *http.Request) {
   574  		_, _ = fmt.Fprintln(w, response)
   575  	}, assert)
   576  }
   577  
   578  func testHeadless(t *testing.T, actions []*Action, timeout time.Duration, handler func(w http.ResponseWriter, r *http.Request), assert func(page *Page, pageErr error, extractedData map[string]string)) {
   579  	t.Helper()
   580  	_ = protocolstate.Init(&types.Options{})
   581  
   582  	browser, err := New(&types.Options{ShowBrowser: false, UseInstalledChrome: testheadless.HeadlessLocal})
   583  	require.Nil(t, err, "could not create browser")
   584  	defer browser.Close()
   585  
   586  	instance, err := browser.NewInstance()
   587  	require.Nil(t, err, "could not create browser instance")
   588  	defer instance.Close()
   589  
   590  	ts := httptest.NewServer(http.HandlerFunc(handler))
   591  	defer ts.Close()
   592  
   593  	input := contextargs.NewWithInput(ts.URL)
   594  	input.CookieJar, err = cookiejar.New(nil)
   595  	require.Nil(t, err)
   596  
   597  	lfa := getBoolFromEnv("LOCAL_FILE_ACCESS", true)
   598  	rna := getBoolFromEnv("RESTRICED_LOCAL_NETWORK_ACCESS", false)
   599  
   600  	extractedData, page, err := instance.Run(input, actions, nil, &Options{Timeout: timeout, Options: &types.Options{AllowLocalFileAccess: lfa, RestrictLocalNetworkAccess: rna}}) // allow file access in test
   601  	assert(page, err, extractedData)
   602  
   603  	if page != nil {
   604  		page.Close()
   605  	}
   606  }
   607  
   608  func TestContainsAnyModificationActionType(t *testing.T) {
   609  	if containsAnyModificationActionType() {
   610  		t.Error("Expected false, got true")
   611  	}
   612  	if containsAnyModificationActionType(ActionClick) {
   613  		t.Error("Expected false, got true")
   614  	}
   615  	if !containsAnyModificationActionType(ActionSetMethod, ActionAddHeader, ActionExtract) {
   616  		t.Error("Expected true, got false")
   617  	}
   618  	if !containsAnyModificationActionType(ActionSetMethod, ActionAddHeader, ActionSetHeader, ActionDeleteHeader, ActionSetBody) {
   619  		t.Error("Expected true, got false")
   620  	}
   621  }
   622  
   623  func TestBlockedHeadlessURLS(t *testing.T) {
   624  
   625  	// run this test from binary since we are changing values
   626  	// of global variables
   627  	if os.Getenv("TEST_BLOCK_HEADLESS_URLS") != "1" {
   628  		cmd := exec.Command(os.Args[0], "-test.run=TestBlockedHeadlessURLS", "-test.v")
   629  		cmd.Env = append(cmd.Env, "TEST_BLOCK_HEADLESS_URLS=1")
   630  		out, err := cmd.CombinedOutput()
   631  		if !strings.Contains(string(out), "PASS\n") || err != nil {
   632  			t.Fatalf("%s\n(exit status %v)", string(out), err)
   633  		}
   634  		return
   635  	}
   636  
   637  	opts := &types.Options{
   638  		AllowLocalFileAccess:       false,
   639  		RestrictLocalNetworkAccess: true,
   640  	}
   641  	err := protocolstate.Init(opts)
   642  	require.Nil(t, err, "could not init protocol state")
   643  
   644  	browser, err := New(&types.Options{ShowBrowser: false, UseInstalledChrome: testheadless.HeadlessLocal})
   645  	require.Nil(t, err, "could not create browser")
   646  	defer browser.Close()
   647  
   648  	instance, err := browser.NewInstance()
   649  	require.Nil(t, err, "could not create browser instance")
   650  	defer instance.Close()
   651  
   652  	ts := httptest.NewServer(nil)
   653  	defer ts.Close()
   654  
   655  	testcases := []string{
   656  		"file:/etc/hosts",
   657  		" file:///etc/hosts\r\n",
   658  		"	fILe:/../../../../etc/hosts",
   659  		ts.URL, // local test server
   660  		"fTP://example.com:21\r\n",
   661  		"ftp://example.com:21",
   662  		"chrome://settings",
   663  		"	chROme://version",
   664  		"chrome-extension://version\r",
   665  		"	chrOme-EXTension://settings",
   666  		"view-source:file:/etc/hosts",
   667  	}
   668  
   669  	for _, testcase := range testcases {
   670  		actions := []*Action{
   671  			{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": testcase}},
   672  			{ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}},
   673  		}
   674  
   675  		data, page, err := instance.Run(contextargs.NewWithInput(ts.URL), actions, nil, &Options{Timeout: 20 * time.Second, Options: opts}) // allow file access in test
   676  		require.Error(t, err, "expected error for url %s got %v", testcase, data)
   677  		require.True(t, stringsutil.ContainsAny(err.Error(), "net::ERR_ACCESS_DENIED", "failed to parse url", "Cannot navigate to invalid URL", "net::ERR_ABORTED", "net::ERR_INVALID_URL"), "found different error %v for testcases %v", err, testcase)
   678  		require.Len(t, data, 0, "expected no data for url %s got %v", testcase, data)
   679  		if page != nil {
   680  			page.Close()
   681  		}
   682  	}
   683  }
   684  
   685  func getBoolFromEnv(key string, defaultValue bool) bool {
   686  	val := os.Getenv(key)
   687  	if val == "" {
   688  		return defaultValue
   689  	}
   690  	return strings.EqualFold(val, "true")
   691  }