Skip to content
GuideBy 11 min read

How to screenshot a page behind a login, safely

A login page is a door. A capture should never pick the lock by typing a password into the form. It should arrive holding a key: proof that someone authorised already signed in. There are four such keys, and they are not equally safe.

Four keys, one door. Each key is a different way to reach the page already signed in, without touching the form.

For pages you are authorised to open

This guide covers your own dashboards, your own accounts and your own staging sites. It does not explain how to get past a login, a bot check, a CAPTCHA or two-factor authentication, and none of the four keys does that. It is not legal advice.

Which key opens a page behind a login?

Four things can open a signed-in page for an automated screenshot: a session cookie, a bearer token, HTTP Basic credentials, or a signed link that expires. Pick one to see the request it makes, what it risks, and where the secret is written down along the way.

Key 1: session cookie, as the site receives it

  1. GET /reports/monthly HTTP/1.1Request line: the page you want
  2. Host: example.comThe site the cookie belongs to
  3. Cookie: session=PASTE_SESSION_COOKIE_VALUEThe secret: proof that someone already signed in

What the server checks

The site looks the value up in its session store. A live session opens the page as that user. An unknown or expired one gets the login page, usually through a redirect.

Use it when

The site has an ordinary login form, single sign-on or two-factor authentication, and offers no tokens or share links. You sign in yourself once and hand over the finished session.

Avoid it when

The only account you have is your own or an administrator account, or the site ties a session to one device or IP address.

Risk if it leaks

3 of 4. Our rating, from the three answers below.

How long does it live?
Until the site ends the session or the user logs out. OWASP suggests idle timeouts of 2 to 5 minutes for high-value applications and 15 to 30 minutes for low-risk ones, but each site decides.[1]
What can it do if it leaks?
Everything the account can do. OWASP calls a session ID "temporarily equivalent to the strongest authentication method used by the application".[1]
Can it be limited to one page?
No. A session covers the whole account. The only limit is the account itself, which is why it should be a dedicated read-only one.

Where does this secret travel?

Five stops between your server and the finished screenshot. Choose a stop to see what happens to key 1 there.

1. Your serverStored, by you

The secret rests here between captures.

Keep it in a secret store or an environment variable, readable by the capture job only. Not in the code, not in the repository.[2]

What happens to each of the four keys at each of the five stops
Stop1. Session cookie2. Bearer token3. HTTP Basic authentication4. Signed or expiring link
1. Your serverStored, by youStored, by youStored, by youStored, by you
2. The requestPasses throughPasses throughPasses throughPasses through
3. Capture servicePasses throughPasses throughPasses throughWritten down
4. Target sitePasses throughPasses throughPasses throughWritten down
5. The screenshotNo secret hereNo secret hereNo secret hereNo secret here
The same trace for all four keys. Only the signed link is written down on the way, because it is the only key that travels in the address. That is acceptable for a link that dies in minutes and for nothing else.
Read every stop for every key

1. Session cookie

  1. Your server: stored, by you. Keep it in a secret store or an environment variable, readable by the capture job only. Not in the code, not in the repository.[2]
  2. The request: passes through. Send it in the JSON body of a POST over HTTPS. A request body is not part of the address, so it stays out of access logs, browser history and Referer headers.[4]
  3. Capture service: passes through. The service needs the real value to set the cookie, so it holds it while the job runs. What it keeps afterwards is the question to ask: its stored record should mask the value.[3]
  4. Target site: passes through. The browser sends the cookie only to hosts that match its Domain and Path, as any browser does. Other hosts the page loads from never see it.[5][7]
  5. The screenshot: no secret here. The image holds no credential, but it shows everything the account can see. Store it like the data it shows.

2. Bearer token

  1. Your server: stored, by you. Keep it in a secret store or an environment variable, readable by the capture job only. Not in the code, not in the repository.[2]
  2. The request: passes through. Send it in the JSON body of a POST over HTTPS. A request body is not part of the address, so it stays out of access logs, browser history and Referer headers.[4]
  3. Capture service: passes through. The service holds the token while the job runs. OWASP lists access tokens among the data that should not be recorded directly in logs, so the stored record should mask it.[3]
  4. Target site: passes through. The header should reach the site in the address and nobody else. Browsers strip Authorization from cross-origin redirects; a capture service must do the same for every other host the page loads from.[6]
  5. The screenshot: no secret here. The image holds no credential, but it shows everything the account can see. Store it like the data it shows.

3. HTTP Basic authentication

  1. Your server: stored, by you. Keep it in a secret store or an environment variable, readable by the capture job only. Not in the code, not in the repository.[2]
  2. The request: passes through. Send it in the JSON body of a POST over HTTPS. A request body is not part of the address, so it stays out of access logs, browser history and Referer headers.[4]
  3. Capture service: passes through. The service holds the encoded pair while the job runs. Masking matters most here: base64 is reversible, so a logged value is a logged password.[3][6]
  4. Target site: passes through. Over https:// the pair is encrypted in transit. Over http:// it crosses the network in clear text, which RFC 7617 names as the main flaw of the scheme.[8]
  5. The screenshot: no secret here. The image holds no credential, but it shows everything the account can see. Store it like the data it shows.

4. Signed or expiring link

  1. Your server: stored, by you. Only the signing key rests here, and it never leaves. Your server makes a fresh link for each capture.[2]
  2. The request: passes through. The link is the url of the request. Send it in a POST body over HTTPS like any other secret.[4]
  3. Capture service: written down. A capture service records which address it rendered, so the link can be written into its history, and into its web logs before that. Some services mask the signature in the stored record; do not count on it. This is why the link must expire soon after the capture.
  4. Target site: written down. The secret is in the request line, and web servers log request lines. OWASP notes that HTTPS does not change this. By the time anyone reads the log, the link should be dead.[4]
  5. The screenshot: no secret here. The image holds no credential, but it shows everything the account can see. Store it like the data it shows.

Session cookie, bearer token, Basic auth or signed link?

The four ways to reach a signed-in page differ in three things that decide the damage of a leak: how long the secret lives, how much it opens, and whether it can be cut down to one page.

Session cookie, bearer token, HTTP Basic authentication and signed link compared
KeyWhat you sendHow long it livesBlast radius if leakedOne page only?Works with SSO or 2FA?Effort
Session cookieRisk: highWhat you sendCookie: session=...How long it livesUntil the session expires or the user logs out. The site decides: OWASP suggests 2 to 30 minutes idle, 4 to 8 hours in total for an office application.[1]Blast radius if leakedThe whole account, with every right it hasOne page only?No. Scope it by using a read-only account.Works with SSO or 2FA?Yes. You complete the sign-in and the second factor yourself; the cookie carries the finished session.EffortLow: copy one value from the browser. Repeat when it expires.
Bearer tokenRisk: mediumWhat you sendAuthorization: Bearer ...How long it livesSet by the issuer. RFC 6750 recommends one hour or less.[9]Blast radius if leakedWhat the token was granted, no moreOne page only?Partly: read-only or narrow scopes where the issuer offers them.Works with SSO or 2FA?Yes, when the site issues tokens. No login step happens at capture time.EffortLow once a token exists. Higher when it must be refreshed every hour.
HTTP Basic authenticationRisk: highestWhat you sendAuthorization: Basic ...How long it livesUntil the password is changedBlast radius if leakedEverything behind that password, and every other place the password is reusedOne page only?NoWorks with SSO or 2FA?No. Basic authentication is a name and a password only.EffortLowest: one value, no expiry to handle.
Signed or expiring linkRisk: lowestWhat you sendA URL with expires and signature in itHow long it livesThe expiry written into the link, for example minutes[12]Blast radius if leakedOne page, read-only, until it expiresOne page only?Yes, by designWorks with SSO or 2FA?Yes. Your server makes the link after its own checks; nobody signs in at capture time.EffortHighest at first: your application has to make and check the links.

The login form is not a fifth key

Typing a name and password into the login form with a script is not on this list. It does not get past single sign-on, a CAPTCHA or two-factor authentication, those checks exist to stop automated sign-ins, and it puts a real password into a script. Do not attempt it. Sign in yourself and hand over the result.

What is the safest way to screenshot a page behind a login?

The safest way to capture a signed-in page is to give the capture the least powerful, shortest-lived secret that still opens it, and to keep that secret on your server. Eight steps, each with its reason.

  1. Pick the weakest key that opens the page

    Prefer an expiring link, then a read-only token, then the session cookie of a dedicated account. Use Basic authentication only for a shared staging password.

    Why. The less a secret can do and the sooner it dies, the less a leak costs.

  2. Make an account or a link only for captures

    Create a new user with read-only rights and access to nothing but the pages you capture. Never borrow a person’s session.

    Why. OWASP: "the Least Privilege principle should be applied." A capture account that leaks can be deleted without locking anyone out.[2]

  3. Keep the secret in server-side configuration

    Store it in a secret manager or an environment variable. Not in front-end code, not in a mobile app, not in the repository.

    Why. Anything shipped to a browser can be read by its user, and OWASP lists secrets hardcoded in source code as the problem secrets management exists to solve.[2]

  4. Send it over HTTPS, in a body or a header

    Call the capture service from your server with POST, and capture an https:// address.

    Why. OWASP: "it is essential to use an encrypted HTTPS (TLS) connection for the entire web session." URLs are kept in web logs, browser history and Referer headers even with HTTPS.[1][4]

  5. Prove you are signed in before the capture

    Wait for an element that exists only on the signed-in page, such as the account menu, and fail when it does not appear.

    Why. An expired session does not produce an error. It produces a sharp screenshot of the login page, which then lands in a report or an email.

  6. Rotate it, and let it expire

    Give tokens and links the shortest life that works, replace long-lived secrets on a schedule, and replace them at once when someone leaves the team.

    Why. OWASP: "You should regularly rotate secrets so that any stolen credentials will only work for a short time."[2]

  7. Treat the screenshot as sensitive data

    Store it where the page’s data would be allowed to live, give download links an expiry, and do not post it to a public channel.

    Why. The image shows whatever the account can see: names, revenue, invoices. A leaked screenshot is a data leak, with or without the secret.

  8. Take the key back when the job ends

    Log the session out, revoke the token or delete the capture account when the captures stop.

    Why. A secret nobody uses still works for whoever finds it.

What should you never do with a login secret?

Seven mistakes turn a harmless screenshot job into a leaked account. Each one is easy to make and each one has a plain reason.

  • Put a password or a session token in a query string

    URLs are stored in web logs, browser history and caches, and travel in Referer headers. OWASP: "Simply using HTTPS does not resolve this vulnerability." RFC 6750 says the same of bearer tokens.[4][9]

  • Reuse an administrator’s personal session

    A session is worth as much as that person’s login. If it leaks, everything an administrator can do is exposed, and ending the session logs a colleague out.[1]

  • Commit a cookie or a token to a repository

    Git keeps every version. A secret that was committed once stays readable in the history after the line is deleted, so it has to be rotated, not removed.[2]

  • Send the secret from front-end code

    Whatever a browser runs, its user can read. A capture request with a cookie in it belongs on your server.

  • Capture over plain http://

    Basic credentials cross the network in clear text, a Secure cookie is not sent at all, and RFC 6750 requires TLS for bearer tokens.[5][8][9]

  • Script the login form, a CAPTCHA or a second factor

    Those checks exist to stop automated sign-ins. Working around them means storing a real password in a script, and on a site that is not yours it breaks its rules.

  • Capture accounts that are not yours

    Everything on this page is for pages you are authorised to open. Using someone else’s session or password without permission is unauthorised access, whatever tool sends the request.

Why does the screenshot show the login page?

A capture of a signed-in page fails in five recognisable ways, and the image tells you which. A rejected session is not an error to the browser: the site answers with a page, and that page gets captured.

The screenshot shows the login page

Causes

  • Domain mismatch. A cookie set without a Domain is host-only: it goes back to the exact host that set it and not to its subdomains.[5][7]
  • Path mismatch. A cookie with Path=/app is sent for /app and /app/reports, not for /reports.[5]
  • Secure on an http:// address. A Secure cookie is sent only with https: requests.[5]
  • The session expired, or the user logged out, which ends the session on the server.[1]
  • A __Host- cookie sent with a Domain. That prefix requires Secure, Path=/ and no Domain attribute.[5]

Fix

Copy the Domain and Path columns exactly as the browser shows them, capture the https:// address, and sign in again for a fresh value. Then add a signed-in check so the next expiry is an error and not an image.

The screenshot shows a 401 or 403 error

Causes

  • The token expired or was revoked, or the scheme word is missing: the value must read Bearer <token> or Basic <base64>.[6]
  • The token is valid but lacks the right for this page, which is a 403.
  • The site redirected to another origin. Browsers strip the Authorization header from cross-origin redirects.[6]

Fix

Request the page with the same header from a terminal first. When that works, capture the final address, after any redirect, and check the rights of the token.

The capture ends on the identity provider, or loops

Causes

  • The application did not recognise the session and sent the browser to its single sign-on provider, a different host that has no session for it.
  • You copied the provider’s cookie and not the application’s. The application keeps its own session cookie on its own domain after the sign-on completes.

Fix

Finish the sign-on yourself in a browser, then copy the session cookie of the application’s own domain. Do not try to replay the sign-on flow. If sessions are short, ask for a service token or build an expiring link.

A "verify it is you" or new-device page appears

Causes

  • The site ties a session to the device, browser or IP address it was created from, and the capture arrives from somewhere else.
  • The site challenges automated browsers before it looks at your session at all.

Fix

This is the site’s protection doing its job, so do not work around it. On your own application, issue a token or a signed link for captures. On someone else’s, ask them for an automation token or an export.

The page is signed in, but the data is missing

Causes

  • A single-page application loads its data after the first paint, sometimes after it refreshes a short-lived token, and the capture came first.
  • The data comes from a second host. A host-only cookie is not sent to api.example.com, and SameSite=Strict or Lax cookies are not sent on cross-site subrequests.[5]
  • The application keeps its token in the browser’s local storage, which a cookie or a header cannot fill.

Fix

Wait for an element that only appears with the data. For a second host on the same site, send the cookie with a Domain that covers both, such as .example.com. For a token in local storage, an expiring link to a server-rendered version of the page is the reliable route.

A session cookie is listed in the developer tools of the browser you signed in with, under Application in Chrome and under Storage in Firefox.

  1. Sign in to the site in a private window, as the account made for captures.
  2. Open the developer tools with F12, or right-click the page and choose Inspect.
  3. In Chrome, open Application, then Storage, then Cookies, and select the site’s origin.[10]
  4. In Firefox, open the Storage panel and choose Cookies.[11]
  5. Find the session cookie. Its name often contains session, sid or auth, and its HttpOnly column is usually ticked.
  6. Copy the Value, and note the Domain, Path and Secure columns: the capture needs them to match.[10][11]
  7. Close the private window without logging out. Logging out ends the session you just copied.
ApplicationStorageCookieshttps://example.com
Example of the cookies table: one session cookie and one preference cookie
NameValueDomainPathHttpOnlySecure
sessions%3Afake-value-7Qd...example.com/✓✓
themelightexample.com/
A drawing of the cookies table in the developer tools, with a made-up session. Chrome and Firefox both show these columns; the highlighted row is the one to copy.

That value is as powerful as your password

While the session lives, anyone who has the cookie is signed in as that account, with no password and no second factor asked. Copy it only from an account made for captures, paste it only into your server's secret store, and never into a chat, a ticket or an email.[1]

How does CurlShot take each key?

CurlShot accepts all four keys through its screenshot API: cookies in cookies, a bearer token or Basic credentials in authorization, any other header in headers, and a signed link as the plain url. It does not sign in for you.

The option that carries each key, its format and its limits
KeyOptionFormatLimits
Session cookieOptioncookiesFormatA list of objects with name and value, plus optional domain, path, secure, http_only, same_site and expires. A cookie can also be one string: "session=...; Domain=example.com; Secure".LimitsUp to 50 cookies, each value up to 4,096 characters. Without domain, the cookie belongs to the host of url.
Bearer tokenOptionauthorizationFormatThe whole header value as one string: "Bearer YOUR_TOKEN".LimitsUp to 4,096 characters. Sent only to the origin of url.
Basic authenticationOptionauthorizationFormat"Basic " followed by the base64 of user:password.LimitsAn address with a name and password in it, such as https://user:[email protected], is refused with invalid_options.
A token in a custom headerOptionheadersFormatAn object of names and values: { "X-Preview-Token": "..." }.LimitsUp to 50 headers, each value up to 4,096 characters. Ten connection-level headers cannot be set, among them Host, Content-Length and Proxy-Authorization.
Signed or expiring linkOptionurlFormatThe link itself. No credential option is needed.LimitsThe address is kept in your screenshot history with the value of a signature, token or password parameter masked. Other parts of the address are stored as sent, so give the link a short expiry.

A request with a session cookie

cURL
curl -X POST "https://curlshot.com/api/v1/screenshot" \
  -H "X-Access-Key: $SCREENSHOT_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/reports/monthly",
    "cookies": [
      {
        "name": "session",
        "value": "PASTE_SESSION_COOKIE_VALUE",
        "domain": "example.com",
        "secure": true
      }
    ],
    "wait_for_selector": "#account-menu",
    "full_page": true
  }' \
  --output report.png

A request with a bearer token

cURL
curl -X POST "https://curlshot.com/api/v1/screenshot" \
  -H "X-Access-Key: $SCREENSHOT_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/reports/monthly",
    "authorization": "Bearer PASTE_READ_ONLY_TOKEN",
    "wait_for_selector": "#account-menu"
  }' \
  --output report.png

Both secrets are placeholders, and the access key comes from an environment variable on your server. wait_for_selector is the signed-in check: if #account-menu never appears, the answer is a selector_not_found error and not a screenshot of the login page. The guide to cookies, headers and HTTP credentials walks through each option, the tool page has a short version, and Customize the page covers what you can change before the capture.

What happens to a secret you send

Credentials go to one origin
The authorization value and any header whose name contains auth, cookie, token, secret, key or password are sent only to requests for the origin of url: the same scheme, host and port. Fonts, analytics and other hosts the page loads from do not receive them, and neither does a site the page redirects to. Cookies follow their Domain, as in any browser.
Stored records are masked
The record of a request that CurlShot keeps for your history stores every cookie value, the authorization value and those credential headers as [redacted]. Cookie names, domains and the other options are kept.
Credentials in the address are masked
The url of a request is saved with the record and shown in your history. The value of a query parameter that is named like a credential (signature, sig, token, access_token, password, api_key, the X-Amz- and X-Goog- signature fields and similar) is stored as redacted. A secret under another name, or in the path, is stored as sent, so keep secrets out of the address or make them expire.
Background jobs hold the request until it is rendered
A job sent with async or through a bulk call waits in a queue with its full options, credentials included, because the renderer needs them. A finished job is removed from the queue within one hour and a failed one within 7 days.
Nothing fills in a login form
There is no option that types a name and a password, and none that answers a CAPTCHA or a second factor. You sign in; the API receives the result.
Private addresses are refused
A url that resolves to localhost, a private range such as 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16, or a link-local address is refused with host_not_allowed. The page must be reachable from the public internet.

Can a cloud service screenshot an intranet page?

A capture service on the public internet cannot open a page on a private network or on localhost, with or without a login, and a careful one refuses the address before it tries.

The refusal is deliberate. A service that fetched any address it was given could be pointed at its own internal network, an attack known as server-side request forgery. Three things work for a page that is not public:

  1. Run a headless browser inside the network, next to the page. It is the only setup that reaches an address the internet cannot.
  2. Render the report to HTML on your own server and send that HTML to the capture service with the html option. No login is involved, because your server already has the data.
  3. Publish the one page through a public https:// host that checks a signed, expiring link, and capture that link.

Screenshot API or your own headless browser compares the first option with a hosted service.

Login and credential terms, defined

Session cookie
A small named value a site gives the browser after a successful login. The browser sends it back with every request, and the site uses it to find the signed-in session. (Browsers also use "session cookie" for any cookie without an expiry date.)[5][7]
Bearer token
A secret string sent in the Authorization header. Whoever holds ("bears") it can use it; no further proof is asked.[9]
Basic authentication
The oldest HTTP login scheme: the name and password joined by a colon, base64-encoded, and sent with every request.[8]
Signed URL
A link that carries an expiry time and a signature computed with a key only the server knows. The server can tell that the link is its own and has not been changed.[12]
Scope
The list of things a credential is allowed to do, such as "read reports". A narrower scope means a smaller leak.
Least privilege
The rule that an account or a secret gets only the rights its job needs, and nothing else.[2]
Blast radius
Everything that is exposed if one secret leaks: which data, which actions, and for how long.
Single sign-on (SSO)
A setup where one identity provider handles the login for many applications. Each application still keeps its own session afterwards.

Questions people ask

How do I take a screenshot of a page that requires a login?

Sign in to the site yourself, then give the capture tool proof of that session: the session cookie, a bearer token, HTTP Basic credentials, or a signed link your application made for that page. The tool opens the page already signed in. Use a dedicated read-only account, keep the secret on your server, and send it over HTTPS.

Is it safe to send a session cookie to a screenshot service?

It carries the same risk as giving that service the login, for as long as the session lives, so limit what the session can do. Use an account made for captures with read-only rights, send the cookie in a POST body over HTTPS from your server, check how the service stores and logs request data, and log the session out when the job ends.

Can a screenshot tool log in through single sign-on or two-factor authentication?

Not by typing credentials, and it should not try. Single sign-on and two-factor checks are built to stop automated logins. What works is to complete the sign-in yourself in a browser and pass the resulting session cookie of the application, or to use a token or an expiring link that the application issues for automation.

Why does my screenshot show the login page and not the dashboard?

The site did not accept the session. The usual causes are an expired session, a cookie sent for the wrong domain or path, a Secure cookie used with an http address, or a logout that ended the session. Copy the Domain and Path exactly as the browser shows them, capture the https address, and sign in again for a fresh cookie.

Where do I find my session cookie?

In the developer tools of your browser. In Chrome, open Application, then Storage, then Cookies, and select the site. In Firefox, open the Storage panel and choose Cookies. Copy the Value of the session cookie and note its Domain and Path. Treat it like a password: while the session lives, it opens the account.

Can a cloud screenshot service capture a page on my intranet or localhost?

No. A service on the public internet cannot reach a private address, and a careful one refuses to try: CurlShot rejects localhost and private network ranges with a host_not_allowed error. Run a headless browser inside the network, send the page as HTML, or publish the one page behind a signed, expiring link.

What does CurlShot do with the cookies and headers I send?

CurlShot sends the authorization value and credential-like headers only to the origin of the page you capture, and cookies only to their domain. In the stored record of the request, cookie values, the authorization value and those headers are replaced with "[redacted]". In the stored address, the value of a parameter named like a credential (signature, token, password) is masked too; a secret under any other name is stored as sent, so keep secrets out of the address or make them expire.

Sources

  1. [1]OWASP Cheat Sheet Series, Session Management Cheat Sheet. Checked .
  2. [2]OWASP Cheat Sheet Series, Secrets Management Cheat Sheet. Checked .
  3. [3]OWASP Cheat Sheet Series, Logging Cheat Sheet. Checked .
  4. [4]OWASP Community, Information exposure through query strings in URL. Checked .
  5. [5]MDN Web Docs, Set-Cookie header. Checked .
  6. [6]MDN Web Docs, Authorization header. Checked .
  7. [7]IETF, RFC 6265: HTTP State Management Mechanism. Checked .
  8. [8]IETF, RFC 7617: The ‘Basic’ HTTP Authentication Scheme. Checked .
  9. [9]IETF, RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage. Checked .
  10. [10]Chrome DevTools, View, add, edit, and delete cookies. Checked .
  11. [11]Firefox Source Docs, Storage Inspector: Cookies. Checked .
  12. [12]Amazon S3 User Guide, Download and upload objects with presigned URLs. Checked .

Screenshot any page in one step

Paste an address into CurlShot and download the screenshot. A free account gives you 100 screenshots every month. No card needed.