Skip to content

Technical audit of the site

Audit of 30 September 2026 at 16:36 (UTC), version 0.6.8, audited code: 50e0aecc81

This audit is internal and automated : these are the industry's standard tools, run on the site's code, with their raw results published unedited. It is not a certification issued by an outside body. Everything is reproducible: the scripts are in the public repository, and the full results file can be downloaded below.

0 WCAG 2.2 accessibility defects (levels A and AA) across 13 pages, tested on mobile and desktop, none overflowing horizontally.

Lighthouse accessibility score: 100 out of 100 on every page.

8 security checks out of 9 passed, 1 failing.

0 known vulnerabilities in the 52 server libraries.

0 issues confirmed by static code analysis (5 findings, all checked).

626 automated tests passed, 0 failed.

Accessibility

Each page is analysed by axe-core 4.13.0, the engine used by browsers and most accessibility audits, against all the automatable rules of WCAG 2.0, 2.1 and 2.2 (levels A and AA) and its best practices. Two pages are viewed logged in, with a test account. Number of defects found:

PageMobile (390 px)Desktop (1366 px)
Home0
50 rules passed
0
50 rules passed
An overview (fictitious portfolio)0
51 rules passed
0
51 rules passed
Security0
34 rules passed
0
34 rules passed
Your data0
34 rules passed
0
34 rules passed
Frequently asked questions0
27 rules passed
0
27 rules passed
Pricing0
21 rules passed
0
21 rules passed
Report on quote0
37 rules passed
0
37 rules passed
Log in0
30 rules passed
0
30 rules passed
Create an account0
32 rules passed
0
32 rules passed
Legal notice0
31 rules passed
0
31 rules passed
This audit page0
41 rules passed
0
41 rules passed
Account area (logged in)0
36 rules passed
0
36 rules passed
Connections (logged in)0
34 rules passed
0
34 rules passed

An automated check does not replace a test with a screen reader: it covers about half of the WCAG criteria. The others (meaning of the text, reading order, actual keyboard use) were reviewed by hand, with no guarantee of completeness.

Performance, best practices and SEO

Scores out of 100 from Lighthouse 13.5.0, Google's tool, with the "mobile" profile (throttled processor, simulated slow 4G network) then "desktop". 90 and above in green, 50 to 89 in orange, below 50 in red.

On mobile

PagePerformancePerf.AccessibilityAccess.Best practicesPract.SEOSEO
Home94100100100
An overview (fictitious portfolio)99100100not indexed*
Security100100100100
Your data100100100100
Frequently asked questions100100100100
Pricing100100100100
Report on quote100100100not indexed*
Log in100100100not indexed*
Create an account100100100not indexed*
Legal notice100100100not indexed*
This audit page100100100100

On desktop

PagePerformancePerf.AccessibilityAccess.Best practicesPract.SEOSEO
Home98100100100
An overview (fictitious portfolio)100100100not indexed*
Security100100100100
Your data100100100100
Frequently asked questions100100100100
Pricing100100100100
Report on quote100100100not indexed*
Log in100100100not indexed*
Create an account100100100not indexed*
Legal notice100100100not indexed*
This audit page100100100100

* These pages are deliberately excluded from search engines (a personal overview, a form, the account pages): Lighthouse scores them low on SEO for precisely this reason.

Security

8 checks out of 9 passed. Each check is run by audit/securite.py against the site served locally (same code, throwaway database, fictitious portfolio): headers and cookies are read from the responses, every route that writes is called from another site, every account page is requested without a session, every rate limit is hammered until it refuses. The measured values are all in the JSON file; a failing check is marked ✗, never removed.

HTTP headers

Recorded on 12 pages, including the wallet connection page. You can check this yourself: browser developer tools (F12), tab Network, response headers.

VerdictCheckMeasured valueWhat it proves
✓Content-Security-Policy: site scripts onlydefault-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self' (on 11 pages)
Wallet connection: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.walletconnect.com https://*.walletconnect.org https://api.web3modal.org https://*.web3modal.org https://imagedelivery.net; font-src 'self' data:; connect-src 'self' wss://*.walletconnect.com wss://*.walletconnect.org https://*.walletconnect.com https://*.walletconnect.org https://api.web3modal.org https://*.web3modal.org https://*.reown.com; frame-src https://verify.walletconnect.com https://verify.walletconnect.org https://secure.walletconnect.org https://secure-mobile.walletconnect.org https://secure-mobile.walletconnect.com; frame-ancestors 'none'; base-uri 'none'; form-action 'self'
No script from another site or written into the page can run: injected content (an address, a token name) does not become code. Inline styles remain allowed (the site uses them).
✓Strict-Transport-Securitymax-age=31536000 (on all 12 pages)Once the site has been seen over HTTPS, the browser refuses to go back to it unencrypted for a year.
✓X-Content-Type-Optionsnosniff (on all 12 pages)The browser does not guess a file's type: a file sent as an image will not be executed.
✓Frames forbidden (frame-ancestors, X-Frame-Options)DENY (on all 12 pages)The site cannot be displayed in a frame on another site: no hijacked clicks (clickjacking).
✓Referrer-Policysame-origin (on all 12 pages)The page address (which may contain your wallet address) is not passed on to other sites.
✓Permissions-Policybrowsing-topics=(), interest-cohort=() (on all 12 pages)No ad tracking by the browser (Topics, FLoC).

Cookies

The three cookies the site sets, as the server sends them:

VerdictCheckMeasured valueWhat it proves
✓Chosen level cookie (bd_niveau)bd_niveau=…; HttpOnly; Max-Age=31536000; Path=/; SameSite=lax; SecureYour beginner / intermediate / expert choice. HttpOnly: unreadable by a script; Secure: never sent unencrypted; SameSite=Lax: not sent with a form coming from another site.
✓Read alerts cookie (bd_alu)bd_alu=…; HttpOnly; Max-Age=31536000; Path=/; SameSite=lax; SecureThe fingerprints of alerts already seen. HttpOnly: unreadable by a script; Secure: never sent unencrypted; SameSite=Lax: not sent with a form coming from another site.
✓Session cookie (logged-in account)pat_session=…; HttpOnly; Max-Age=43200; Path=/; SameSite=lax; SecureYour session, signed by the server. HttpOnly: unreadable by a script; Secure: never sent unencrypted; SameSite=Lax: not sent with a form coming from another site.

Forms and request origin

VerdictCheckMeasured valueWhat it proves
✓Writes refused from another site (CSRF)34 routes out of 34 refuse a request coming from https://ailleurs.example (403) and accept it from the siteAnother site cannot make your browser act without your knowledge: remove an address, change your nickname, delete your account, pay. Every route that writes is called twice, from a foreign origin then from the site, and the two responses are compared.
The 34 routes tested
RouteFrom another siteFrom the siteMechanism
✓ POST /admin/bonus/{ident}403403safeguard (auth/protect.py), before the application
✓ POST /admin/frequentation/import403404safeguard (auth/protect.py), before the application
✓ POST /admin/frequentation/reconstituer403404safeguard (auth/protect.py), before the application
✓ POST /admin/frequentation/systeme/importer403404safeguard (auth/protect.py), before the application
✓ POST /alertes/abonner403401safeguard (auth/protect.py), before the application
✓ POST /alertes/desabonner403200safeguard (auth/protect.py), before the application
✓ POST /api/connections403401safeguard (auth/protect.py), before the application
✓ POST /api/parcours403204safeguard (auth/protect.py), before the application
✓ POST /api/wallet403401safeguard (auth/protect.py), before the application
✓ POST /auth/wallet403400safeguard (auth/protect.py), before the application
✓ POST /bilan/ajouter403400safeguard (auth/protect.py), before the application
✓ POST /bilan/retirer403400safeguard (auth/protect.py), before the application
✓ POST /billing/checkout403401safeguard (auth/protect.py), before the application
✓ POST /billing/crypto403401safeguard (auth/protect.py), before the application
✓ POST /billing/portal403401safeguard (auth/protect.py), before the application
✓ POST /compte/mot-de-passe403401safeguard (auth/protect.py), before the application
✓ POST /compte/pseudo403401safeguard (auth/protect.py), before the application
✓ POST /compte/supprimer403401safeguard (auth/protect.py), before the application
✓ POST /connexion403400safeguard (auth/protect.py), before the application
✓ POST /contact403400safeguard (auth/protect.py), before the application
✓ POST /declaration/{declaration_id}/acheter403401safeguard (auth/protect.py), before the application
✓ POST /espace/adresse/retirer403401safeguard (auth/protect.py), before the application
✓ POST /inscription403400safeguard (auth/protect.py), before the application
✓ POST /logout403303safeguard (auth/protect.py), before the application
✓ POST /mot-de-passe-oublie403400safeguard (auth/protect.py), before the application
✓ POST /offre/majeur403303safeguard (auth/protect.py), before the application
✓ POST /plateformes/connecter403401safeguard (auth/protect.py), before the application
✓ POST /plateformes/importer403401safeguard (auth/protect.py), before the application
✓ POST /plateformes/ledger403401safeguard (auth/protect.py), before the application
✓ POST /plateformes/{ident}/retirer403401safeguard (auth/protect.py), before the application
✓ POST /reinitialiser403400safeguard (auth/protect.py), before the application
✓ POST /retour403200safeguard (auth/protect.py), before the application
✓ POST /verifier/renvoyer403303safeguard (auth/protect.py), before the application
✓ POST /webhooks/stripe400400webhook signature

Access to account pages

VerdictCheckMeasured valueWhat it proves
✓Account pages and actions without a session35 routes out of 35 respond 401, 403, 404 or redirect to loginAccount area, exchanges, tax returns, payment, administration: nothing is displayed or run without being logged in. Administration pages respond "not found" to any other account.
The 35 routes tested
RouteResponse without a session
✓ GET /admin/bonus403
✓ GET /admin/contact404
✓ GET /admin/frequentation303 → /login
✓ GET /admin/liquidite303 → /login
✓ GET /admin/retours404
✓ GET /api/connections401
✓ GET /api/crypto/paiements401
✓ GET /billing303 → /connexion
✓ GET /billing/crypto303 → /connexion
✓ GET /compte/mot-de-passe303 → /connexion
✓ GET /compte/pseudo303 → /connexion
✓ GET /compte/supprimer303 → /connexion
✓ GET /dashboard303 → /connexion
✓ GET /declaration303 → /connexion
✓ GET /declaration/{declaration_id}303 → /connexion
✓ GET /declaration/{declaration_id}/telecharger303 → /connexion
✓ GET /me303 → /connexion
✓ POST /admin/bonus/{ident}403
✓ POST /admin/frequentation/import404
✓ POST /admin/frequentation/reconstituer404
✓ POST /admin/frequentation/systeme/importer404
✓ POST /api/connections401
✓ POST /billing/checkout401
✓ POST /billing/crypto401
✓ POST /billing/portal401
✓ POST /compte/mot-de-passe401
✓ POST /compte/pseudo401
✓ POST /compte/supprimer401
✓ POST /declaration/{declaration_id}/acheter401
✓ POST /espace/adresse/retirer401
✓ POST /offre/majeur303 → /connexion
✓ POST /plateformes/connecter401
✓ POST /plateformes/importer401
✓ POST /plateformes/ledger401
✓ POST /plateformes/{ident}/retirer401

Rate limits

Measured by hammering each route from a single IP address, until the first refusal:

VerdictCheckMeasured valueWhat it proves
✓Rate limit on /connexionlimit 20 per 15 minutes; refusal (429) measured at request no. 21Brute force on passwords: beyond that, the server responds 429 for a quarter of an hour.
✓Rate limit on /connexion (same email)limit 5 per 15 minutes; refusal (429) measured at request no. 6A single targeted account: five attempts per quarter of an hour and per IP address, then the account is locked after ten failures.
✓Rate limit on /auth/walletlimit 30 per 15 minutes; refusal (429) measured at request no. 31Wallet logins: same limit as the account forms.
✓Rate limit on /bilanlimit 120 per minute; refusal (429) measured at request no. 121A bot chaining overviews is stopped: the global limit per identity kicks in before the route's own limit (150 per hour), then the anonymous IP is limited in overviews per hour and per day.

General limit: 120 requests per minute and per identity; without an account, 100 overviews per hour and 1000 per day for one IP, 10000 per day for the whole site. On top of this come script-cadence detection and temporary banning (auth/protect.py).

Secrets

VerdictCheckMeasured valueWhat it proves
✓No secrets in the code118 files scanned, 8 patterns (Stripe, AWS, GitHub keys, private keys, plain-text passwords…): 0 secrets found, 2 matches dismissed after review; 71 environment variables readAPI keys, passwords and session secrets come from the server environment, never from the published code: the public repository contains nothing worth stealing.
  • bilan/cex_api.py, line 51, PEM private key: -----BEGIN EC PRIVATE KEY, dismissed: help text of the Coinbase form: the expected key format, shown to the user
  • web/cex.py, line 561, PEM private key: -----BEGIN EC PRIVATE KEY, dismissed: label of the "Private key" field of the Coinbase form: the expected format, not a key

Libraries

VerdictCheckMeasured valueWhat it proves
✓No known vulnerabilities in the librariespip-audit (PyPA / OSV database) 2.10.1: 52 libraries, 0 vulnerabilitiesEach server library is checked against the database of published vulnerabilities (PyPA, OSV).
✗Library versions pinned0 pinned (==), 20 with a lower bound only (>=) out of 20: fastapi>=0.115, uvicorn[standard]>=0.30, httpx>=0.27, joserfc>=1.0, sqlalchemy>=2.0, itsdangerous>=2.2, …Pinned versions guarantee that the deployed server is exactly the one audited. With only a lower bound, each deployment may install a newer version than the one tested here: an open issue, to be fixed.

Static analysis

VerdictCheckMeasured valueWhat it proves
✓Static code analysis (bandit)bandit (medium and high severity) 1.9.4, 27168 lines: 5 findings, high: B413 ×2; medium: B310 ×1, B314 ×2 ; 0 confirmed after reviewThe code is screened for known dangerous patterns (injections, weak hashes, deserialisation, XML). Each medium or high severity finding is read; those dismissed are listed with their reason, below.
  • B413 (2), pycryptodome (maintained package, compatible API) is mistaken for the old pyCrypto: only Keccak-256 is used from it, to check Ethereum addresses.
  • B310 (1), urlopen only opens https addresses fixed in the code (Bitcoin explorers, prices): no scheme comes from the user.
  • B314 (2), public news RSS feeds read with the standard library, whose parser (expat ≥ 2.4) blocks chained entity expansion; no external entity is resolved.

What this audit does not cover

  • No penetration test: nobody has tried to attack the site in production. The security checks verify that the announced protections are in place; they do not look for business logic flaws.
  • The site is audited on a local copy of the published code, with a fictitious portfolio: real response times also depend on the host and the data sources (blockchains, exchanges).
  • The configuration of the host (DigitalOcean) and of the database is not visible from here.

Rerun the audit

The scripts are in the audit/ folder of the public repository: npm install then ./lancer.sh. They start the site locally, with the same tools in the same versions, and rewrite the results file.

Download the full results (JSON) : every score, every rule, every defect with the element concerned.