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.
For pages you are authorised to open
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
GET /reports/monthly HTTP/1.1Request line: the page you wantHost: example.comThe site the cookie belongs toCookie: 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]
| Stop | 1. Session cookie | 2. Bearer token | 3. HTTP Basic authentication | 4. Signed or expiring link |
|---|---|---|---|---|
| 1. Your server | Stored, by you | Stored, by you | Stored, by you | Stored, by you |
| 2. The request | Passes through | Passes through | Passes through | Passes through |
| 3. Capture service | Passes through | Passes through | Passes through | Written down |
| 4. Target site | Passes through | Passes through | Passes through | Written down |
| 5. The screenshot | No secret here | No secret here | No secret here | No secret here |
Read every stop for every key
1. Session cookie
- 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]
- The request: passes through. Send it in the JSON body of a
POSTover HTTPS. A request body is not part of the address, so it stays out of access logs, browser history andRefererheaders.[4] - 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]
- Target site: passes through. The browser sends the cookie only to hosts that match its
DomainandPath, as any browser does. Other hosts the page loads from never see it.[5][7] - 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
- 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]
- The request: passes through. Send it in the JSON body of a
POSTover HTTPS. A request body is not part of the address, so it stays out of access logs, browser history andRefererheaders.[4] - 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]
- Target site: passes through. The header should reach the site in the address and nobody else. Browsers strip
Authorizationfrom cross-origin redirects; a capture service must do the same for every other host the page loads from.[6] - 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
- 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]
- The request: passes through. Send it in the JSON body of a
POSTover HTTPS. A request body is not part of the address, so it stays out of access logs, browser history andRefererheaders.[4] - 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]
- Target site: passes through. Over
https://the pair is encrypted in transit. Overhttp://it crosses the network in clear text, which RFC 7617 names as the main flaw of the scheme.[8] - 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
- 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]
- The request: passes through. The link is the
urlof the request. Send it in aPOSTbody over HTTPS like any other secret.[4] - 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.
- 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]
- 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.
| Key | What you send | How long it lives | Blast radius if leaked | One page only? | Works with SSO or 2FA? | Effort |
|---|---|---|---|---|---|---|
| Session cookieRisk: high | What 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 has | One 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: medium | What 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 more | One 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: highest | What you sendAuthorization: Basic ... | How long it livesUntil the password is changed | Blast radius if leakedEverything behind that password, and every other place the password is reused | One page only?No | Works 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: lowest | What you sendA URL with expires and signature in it | How long it livesThe expiry written into the link, for example minutes[12] | Blast radius if leakedOne page, read-only, until it expires | One page only?Yes, by design | Works 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
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.
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.
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]
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]
Send it over HTTPS, in a body or a header
Call the capture service from your server with
POST, and capture anhttps://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
Refererheaders even with HTTPS.[1][4]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.
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]
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.
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
Refererheaders. 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
Securecookie 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
Domainis 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=/appis sent for/appand/app/reports, not for/reports.[5] Secureon anhttp://address. ASecurecookie is sent only withhttps:requests.[5]- The session expired, or the user logged out, which ends the session on the server.[1]
- A
__Host-cookie sent with aDomain. That prefix requiresSecure,Path=/and noDomainattribute.[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>orBasic <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
Authorizationheader 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, andSameSite=StrictorLaxcookies 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.
How do you find your session cookie?
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.
- Sign in to the site in a private window, as the account made for captures.
- Open the developer tools with
F12, or right-click the page and choose Inspect. - In Chrome, open Application, then Storage, then Cookies, and select the site’s origin.[10]
- In Firefox, open the Storage panel and choose Cookies.[11]
- Find the session cookie. Its name often contains
session,sidorauth, and its HttpOnly column is usually ticked. - Copy the Value, and note the Domain, Path and Secure columns: the capture needs them to match.[10][11]
- Close the private window without logging out. Logging out ends the session you just copied.
| Name | Value | Domain | Path | HttpOnly | Secure |
|---|---|---|---|---|---|
| session | s%3Afake-value-7Qd... | example.com | / | ✓ | ✓ |
| theme | light | example.com | / |
That value is as powerful as your password
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.
| Key | Option | Format | Limits |
|---|---|---|---|
| Session cookie | Optioncookies | FormatA 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 token | Optionauthorization | FormatThe whole header value as one string: "Bearer YOUR_TOKEN". | LimitsUp to 4,096 characters. Sent only to the origin of url. |
| Basic authentication | Optionauthorization | Format"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 header | Optionheaders | FormatAn 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 link | Optionurl | FormatThe 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 -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.pngA request with a bearer token
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.pngBoth 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
authorizationvalue and any header whose name containsauth,cookie,token,secret,keyorpasswordare sent only to requests for the origin ofurl: 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 theirDomain, as in any browser. - Stored records are masked
- The record of a request that CurlShot keeps for your history stores every cookie value, the
authorizationvalue and those credential headers as[redacted]. Cookie names, domains and the other options are kept. - Credentials in the address are masked
- The
urlof 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, theX-Amz-andX-Goog-signature fields and similar) is stored asredacted. 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
asyncor 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
urlthat resolves tolocalhost, a private range such as10.0.0.0/8,172.16.0.0/12or192.168.0.0/16, or a link-local address is refused withhost_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:
- Run a headless browser inside the network, next to the page. It is the only setup that reaches an address the internet cannot.
- Render the report to HTML on your own server and send that HTML to the capture service with the
htmloption. No login is involved, because your server already has the data. - 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
Authorizationheader. 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]OWASP Cheat Sheet Series, Session Management Cheat Sheet. Checked .
- [2]OWASP Cheat Sheet Series, Secrets Management Cheat Sheet. Checked .
- [3]OWASP Cheat Sheet Series, Logging Cheat Sheet. Checked .
- [4]OWASP Community, Information exposure through query strings in URL. Checked .
- [5]MDN Web Docs, Set-Cookie header. Checked .
- [6]MDN Web Docs, Authorization header. Checked .
- [7]IETF, RFC 6265: HTTP State Management Mechanism. Checked .
- [8]IETF, RFC 7617: The ‘Basic’ HTTP Authentication Scheme. Checked .
- [9]IETF, RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage. Checked .
- [10]Chrome DevTools, View, add, edit, and delete cookies. Checked .
- [11]Firefox Source Docs, Storage Inspector: Cookies. Checked .
- [12]Amazon S3 User Guide, Download and upload objects with presigned URLs. Checked .
Read next
- CalculatorScreenshot API vs headless browser: the real costA cost calculator with every assumption editable, the hidden work behind headless Chrome, and an honest checklist for build or buy.
- Field guideWhy website screenshots look wrong, and the fixesA field guide to 11 ways a website screenshot goes wrong, from blank lazy images to cookie banners and cut-off pages, with the cause and the fix for each.
- ChecklistCapture a web page as evidence that holds upA checklist for saving a web page as a credible record: what to capture, how to fix the time with a hash and timestamp, and what makes a screenshot weak.