Technical audit of the site
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:
| Page | Mobile (390 px) | Desktop (1366 px) |
|---|---|---|
| Home | 0 50 rules passed | 0 50 rules passed |
| An overview (fictitious portfolio) | 0 51 rules passed | 0 51 rules passed |
| Security | 0 34 rules passed | 0 34 rules passed |
| Your data | 0 34 rules passed | 0 34 rules passed |
| Frequently asked questions | 0 27 rules passed | 0 27 rules passed |
| Pricing | 0 21 rules passed | 0 21 rules passed |
| Report on quote | 0 37 rules passed | 0 37 rules passed |
| Log in | 0 30 rules passed | 0 30 rules passed |
| Create an account | 0 32 rules passed | 0 32 rules passed |
| Legal notice | 0 31 rules passed | 0 31 rules passed |
| This audit page | 0 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 |
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
| Page | PerformancePerf. | AccessibilityAccess. | Best practicesPract. | SEOSEO |
|---|---|---|---|---|
| Home | 94 | 100 | 100 | 100 |
| An overview (fictitious portfolio) | 99 | 100 | 100 | not indexed* |
| Security | 100 | 100 | 100 | 100 |
| Your data | 100 | 100 | 100 | 100 |
| Frequently asked questions | 100 | 100 | 100 | 100 |
| Pricing | 100 | 100 | 100 | 100 |
| Report on quote | 100 | 100 | 100 | not indexed* |
| Log in | 100 | 100 | 100 | not indexed* |
| Create an account | 100 | 100 | 100 | not indexed* |
| Legal notice | 100 | 100 | 100 | not indexed* |
| This audit page | 100 | 100 | 100 | 100 |
On desktop
| Page | PerformancePerf. | AccessibilityAccess. | Best practicesPract. | SEOSEO |
|---|---|---|---|---|
| Home | 98 | 100 | 100 | 100 |
| An overview (fictitious portfolio) | 100 | 100 | 100 | not indexed* |
| Security | 100 | 100 | 100 | 100 |
| Your data | 100 | 100 | 100 | 100 |
| Frequently asked questions | 100 | 100 | 100 | 100 |
| Pricing | 100 | 100 | 100 | 100 |
| Report on quote | 100 | 100 | 100 | not indexed* |
| Log in | 100 | 100 | 100 | not indexed* |
| Create an account | 100 | 100 | 100 | not indexed* |
| Legal notice | 100 | 100 | 100 | not indexed* |
| This audit page | 100 | 100 | 100 | 100 |
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.
| Verdict | Check | Measured value | What it proves |
|---|---|---|---|
| ✓ | Content-Security-Policy: site scripts only | default-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-Security | max-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-Options | nosniff (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-Policy | same-origin (on all 12 pages) | The page address (which may contain your wallet address) is not passed on to other sites. |
| ✓ | Permissions-Policy | browsing-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:
| Verdict | Check | Measured value | What it proves |
|---|---|---|---|
| ✓ | Chosen level cookie (bd_niveau) | bd_niveau=…; HttpOnly; Max-Age=31536000; Path=/; SameSite=lax; Secure | Your 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; Secure | The 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; Secure | Your 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
| Verdict | Check | Measured value | What 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 site | Another 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
| Route | From another site | From the site | Mechanism |
|---|---|---|---|
✓ POST /admin/bonus/{ident} | 403 | 403 | safeguard (auth/protect.py), before the application |
✓ POST /admin/frequentation/import | 403 | 404 | safeguard (auth/protect.py), before the application |
✓ POST /admin/frequentation/reconstituer | 403 | 404 | safeguard (auth/protect.py), before the application |
✓ POST /admin/frequentation/systeme/importer | 403 | 404 | safeguard (auth/protect.py), before the application |
✓ POST /alertes/abonner | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /alertes/desabonner | 403 | 200 | safeguard (auth/protect.py), before the application |
✓ POST /api/connections | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /api/parcours | 403 | 204 | safeguard (auth/protect.py), before the application |
✓ POST /api/wallet | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /auth/wallet | 403 | 400 | safeguard (auth/protect.py), before the application |
✓ POST /bilan/ajouter | 403 | 400 | safeguard (auth/protect.py), before the application |
✓ POST /bilan/retirer | 403 | 400 | safeguard (auth/protect.py), before the application |
✓ POST /billing/checkout | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /billing/crypto | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /billing/portal | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /compte/mot-de-passe | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /compte/pseudo | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /compte/supprimer | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /connexion | 403 | 400 | safeguard (auth/protect.py), before the application |
✓ POST /contact | 403 | 400 | safeguard (auth/protect.py), before the application |
✓ POST /declaration/{declaration_id}/acheter | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /espace/adresse/retirer | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /inscription | 403 | 400 | safeguard (auth/protect.py), before the application |
✓ POST /logout | 403 | 303 | safeguard (auth/protect.py), before the application |
✓ POST /mot-de-passe-oublie | 403 | 400 | safeguard (auth/protect.py), before the application |
✓ POST /offre/majeur | 403 | 303 | safeguard (auth/protect.py), before the application |
✓ POST /plateformes/connecter | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /plateformes/importer | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /plateformes/ledger | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /plateformes/{ident}/retirer | 403 | 401 | safeguard (auth/protect.py), before the application |
✓ POST /reinitialiser | 403 | 400 | safeguard (auth/protect.py), before the application |
✓ POST /retour | 403 | 200 | safeguard (auth/protect.py), before the application |
✓ POST /verifier/renvoyer | 403 | 303 | safeguard (auth/protect.py), before the application |
✓ POST /webhooks/stripe | 400 | 400 | webhook signature |
Access to account pages
| Verdict | Check | Measured value | What it proves |
|---|---|---|---|
| ✓ | Account pages and actions without a session | 35 routes out of 35 respond 401, 403, 404 or redirect to login | Account 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
| Route | Response without a session |
|---|---|
✓ GET /admin/bonus | 403 |
✓ GET /admin/contact | 404 |
✓ GET /admin/frequentation | 303 → /login |
✓ GET /admin/liquidite | 303 → /login |
✓ GET /admin/retours | 404 |
✓ GET /api/connections | 401 |
✓ GET /api/crypto/paiements | 401 |
✓ GET /billing | 303 → /connexion |
✓ GET /billing/crypto | 303 → /connexion |
✓ GET /compte/mot-de-passe | 303 → /connexion |
✓ GET /compte/pseudo | 303 → /connexion |
✓ GET /compte/supprimer | 303 → /connexion |
✓ GET /dashboard | 303 → /connexion |
✓ GET /declaration | 303 → /connexion |
✓ GET /declaration/{declaration_id} | 303 → /connexion |
✓ GET /declaration/{declaration_id}/telecharger | 303 → /connexion |
✓ GET /me | 303 → /connexion |
✓ POST /admin/bonus/{ident} | 403 |
✓ POST /admin/frequentation/import | 404 |
✓ POST /admin/frequentation/reconstituer | 404 |
✓ POST /admin/frequentation/systeme/importer | 404 |
✓ POST /api/connections | 401 |
✓ POST /billing/checkout | 401 |
✓ POST /billing/crypto | 401 |
✓ POST /billing/portal | 401 |
✓ POST /compte/mot-de-passe | 401 |
✓ POST /compte/pseudo | 401 |
✓ POST /compte/supprimer | 401 |
✓ POST /declaration/{declaration_id}/acheter | 401 |
✓ POST /espace/adresse/retirer | 401 |
✓ POST /offre/majeur | 303 → /connexion |
✓ POST /plateformes/connecter | 401 |
✓ POST /plateformes/importer | 401 |
✓ POST /plateformes/ledger | 401 |
✓ POST /plateformes/{ident}/retirer | 401 |
Rate limits
Measured by hammering each route from a single IP address, until the first refusal:
| Verdict | Check | Measured value | What it proves |
|---|---|---|---|
| ✓ | Rate limit on /connexion | limit 20 per 15 minutes; refusal (429) measured at request no. 21 | Brute 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. 6 | A 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/wallet | limit 30 per 15 minutes; refusal (429) measured at request no. 31 | Wallet logins: same limit as the account forms. |
| ✓ | Rate limit on /bilan | limit 120 per minute; refusal (429) measured at request no. 121 | A 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. |
Secrets
| Verdict | Check | Measured value | What it proves |
|---|---|---|---|
| ✓ | No secrets in the code | 118 files scanned, 8 patterns (Stripe, AWS, GitHub keys, private keys, plain-text passwords…): 0 secrets found, 2 matches dismissed after review; 71 environment variables read | API keys, passwords and session secrets come from the server environment, never from the published code: the public repository contains nothing worth stealing. |
Libraries
| Verdict | Check | Measured value | What it proves |
|---|---|---|---|
| ✓ | No known vulnerabilities in the libraries | pip-audit (PyPA / OSV database) 2.10.1: 52 libraries, 0 vulnerabilities | Each server library is checked against the database of published vulnerabilities (PyPA, OSV). |
| ✗ | Library versions pinned | 0 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
| Verdict | Check | Measured value | What 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 review | The 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. |
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.