State of security in this repo — what's here, what isn't, and what to watch.
This doc captures the observable surface. Strix and Shannon audits have been run; their output lives outside the repo.
Files: src/utils/constants.ts (runtime) and
scripts/buildUtils/api/getEndpoint.js (build-time copy).
export const CLIENT_AUTH_SECRET = '952b4412f002315aa50751032fcaab03'Used by src/api/utils.ts::getClientAuthHash to sign unauthenticated API
requests (authenticated requests skip client_auth_hash and send
Authorization: Bearer <session_auth_hash> instead):
const getClientAuthHash = (time: string) =>
md5(`${CLIENT_AUTH_SECRET}${time}`).toString()This secret ships with every extension build — unzip any production
build.zip, open background.bundle.js, and you can extract it. The API
treats client_auth_hash as a soft replay-mitigation, not a real
authentication factor; the real auth is session_auth_hash on the
user's session.
Don't rely on CLIENT_AUTH_SECRET for anything that actually requires
secrecy. If we ever need per-build or per-install secrets, they can't live
in the bundle.
A separate constant, SECRET_KEY_PSY_OP
('if_you_copy_this_you_might_die_a_painful_death'), lives in both
scripts/login.js and src/utils/constants.ts. It is a different value,
used to sign the AuthToken/login secure token (at build time in
scripts/login.js, and at runtime by the login thunk). It is also
observable from unzipping.
.env is not tracked (it is in .gitignore). Create it locally with the
fields listed in the README. In CI,
.gitlab-ci.yml writes it:
- The
testjob appendsNODE_ENV,API_URL,BACKUP_API_URL(from the$BACKUP_API_URLCI variable), and theTEST_USER_*/BUILD_USER_*credentials. build:extensionappendsNODE_ENV,API_URL, andBACKUP_API_URL=totallyacdn.com.
REACT_APP_REDUX_PORT and BROWSER are not written in CI; they fall back
to the defaults in src/utils/constants.ts.
Hard rules that still apply:
- Never commit a
.envor.env.local. They hold your personal build and test credentials. - Keep real secrets in GitLab CI variables, like
TEST_USER_*/BUILD_USER_*already are in.gitlab-ci.yml:test: script: # ... runner diagnostics ... - printf "\nNODE_ENV=production" >> ".env" - printf "\nAPI_URL=windscribe.com" >> ".env" - printf "\nBACKUP_API_URL=$BACKUP_API_URL" >> ".env" - printf "\nTEST_USER_NAME=$TEST_USER_NAME" >> ".env" - printf "\nTEST_USER_PASSWORD=$TEST_USER_PASSWORD" >> ".env" - printf "\nBUILD_USER_NAME=$BUILD_USER_NAME" >> ".env" - printf "\nBUILD_USER_PASSWORD=$BUILD_USER_PASSWORD" >> ".env"
Server credentials (the proxy's basic-auth username/password) come from
the /ServerCredentials API call and are stored in state.serverCredentials
base64-encoded. Base64 is not encryption — anyone with read access to
chrome.storage can decode them.
Decoded at response time in authRequiredHandler:
callback({
authCredentials: { username: atob(username), password: atob(password) },
})This is fine given the threat model — they're ephemeral per-user proxy credentials tied to the session, not the user's login password.
The user's login password is not stored — it's exchanged at login for
a session_auth_hash.
.gitlab-ci.yml currently declares four stages
(prepare_environment, install_packages, test, build) and the
following jobs:
build:image(stageprepare_environment, only runs onmasterwhenDockerfilechanges)install(stageinstall_packages)eslint(stagetest)test(stagetest, e2e)build:extension(stagebuild)
SAST, Secret Detection, and Dependency Scanning: resolved internally.
File: .husky/pre-commit.
yarn run lintOnly runs ESLint. If we add secret scanning, a minimal addition would be:
yarn run lint
gitleaks protect --staged --redact…guarded on gitleaks being installed locally. Opt-in until the whole team
has the binary.
src/manifest.json declares a lot of permissions (see
architecture.md). Each one is actually
used — we verified this while writing the docs. Keep this in mind when
reviewing PRs: adding a new permission is a CWS review slowdown, and
host_permissions: <all_urls> is as broad as it gets already.
contentSettings and browsingData are kept as optional_permissions.
Each one is requested only when the user turns on the matching action.
All warps (language, location, timezone, split personality) and
anti-fingerprinting scripts run with world: 'MAIN', i.e. in the page's
own JS world rather than an isolated extension world. The exception is
fingerprintMessageListener.bundle.js (fingerprintMessageListenerScriptId),
which is registered with world: 'ISOLATED' (in registerScripts.ts,
Privacy.tsx, and useInitialDataFetching.ts). MAIN is required for
monkey-patching navigator.language / Intl.DateTimeFormat /
HTMLCanvasElement.prototype.toDataURL etc. — the patches have to be
visible to page code.
Implication: a malicious page that's watching for these patches can detect the extension. This is a known tradeoff for the anti-fingerprinting features.
Scripts run at document_start so a page can't read the unpatched globals
before we get to them.
None used in src/. A grep for eval( / new Function( turns up only
library usage (if anything) — keep it that way; MV3 forbids them in the
SW anyway.
src/services/proxyConfig/proxyConfig.ts generates a PAC script that is
handed to chrome.proxy.settings.set. Two notes:
- The PAC contains debug
alert()calls (alert('proxy url: ' + url)etc.). Their output is network-level only: it appears only in Chrome's network log export (chrome://net-export), not in DevTools, and the user never sees an alert. They ship in production builds. This is accepted. It is not a security issue. - Windscribe's own API domains are hardcoded in the PAC's allowlist to prevent the "proxy-through-proxy" chicken-and-egg on reconnect. This is required behaviour, not a leak.
The submodule is vendored from our own mirror
(gitlab.int.windscribe.com/.../ubo-lite-mirror) rather than upstream
GitHub. This means:
- Any supply-chain compromise of upstream requires manual action on our side — we don't pull transparently.
- Our Windscribe patches (see
ubo-lite-mirror.md) are reviewable in one place.
But:
- If the mirror itself is compromised, or if someone lands a patch on the mirror without review, we ship it on the next build. The submodule pinning plus manual bumps are the only guard.
None open.
- Remove the stray
console.log('onMessage - ublock', ...)from the ubo-lite patch (mirror commit59f9d8eb). -
windscribeContentScriptIdsinubo-lite-mirror/chromium/js/scripting-manager.jsis a single list. - Run Strix audit (owner: Ahmed).
- Run Shannon audit (owner: Ahmed).
- SAST / Secret Detection / Dependency Scanning: resolved internally.
- Not needed: gating PAC
alert()s onNODE_ENV === 'development'.
api.md— whereCLIENT_AUTH_SECRETis used.background-service-worker.md—webRequest.onAuthRequiredandwebRequestAuthProvider.proxy-and-pac.md— the PAC alerts.ubo-lite-mirror.md— submodule maintenance.