Security
Last updated
Two threat models
A QR generator sits between two people with different exposure. The first types something sensitive into a form. The second points a camera at a printed square and lets their phone act on whatever comes out. Both halves are treated as security problems here; the second is the one the industry mostly ignores.
For the person generating a code
The input is the sensitive part: a Wi-Fi password, a home address in a contact card, a wallet address, a private calendar invitation. The protection is structural rather than procedural — there is no endpoint to receive any of it, no database to store it and no server-side code in the deployment at all.
- The source is scanned before release for the browser's request primitives — fetch, WebSocket, beacon and the rest — and for identifiers that describe sending bytes anywhere. A match stops the release rather than raising a warning.
- The published files are scanned for third-party origins. There is no CDN, no web font from elsewhere, no analytics endpoint and no ad script, so no third party is in the request path to begin with.
connect-src 'self'in the Content Security Policy means that even if code tried to open a connection to another origin, the browser would refuse it.form-action 'none'means the browser would refuse to submit a form on this site anywhere at all. This is why the site has no contact form.
The methodology page is the practical version of this section: it tells you how to watch the network yourself and how to generate a code with the network switched off entirely.
For the person scanning a code
A QR code is an opaque link. The person scanning it cannot read the payload before their phone acts on it, and that is precisely why QR phishing works: a sticker over a parking meter, a forged menu code, a fake invoice. No generator can make a destination safe, and this one does not pretend to verify that a URL you encode is live, lawful or yours.
What it does instead is refuse to make a hostile code easier to build:
- Only http and https addresses are encoded. Any other scheme is refused with the reason stated, so the site will not produce a code that runs script or reaches into a device's own storage when scanned. A generator will not encode an address it cannot parse either.
- Whitespace and control characters in an address are refused. A reader ends a URI at the first one, so a code containing them would decode to a truncated destination that still looks plausible on the printed page.
- The exact string is on screen before you export. It is shown as selectable text with its byte count and a copy button. There is no transformation between what that panel shows and what the symbol contains, so a destination cannot be one thing in the form and another in the code.
- No shortening and no wrapping. The symbol encodes the address you gave it. It does not route through this domain, so the printed code is as inspectable as the string you can read in the panel.
Note what is not claimed: the site does not check whether a hostname uses look-alike characters from another script, and no generator can tell you whether a destination is trustworthy.
So if you are the one scanning, the checks stay with you: read the address your camera shows you before opening it, be suspicious of a code stuck on top of another code, and never enter a password or a payment detail on a page you reached only by scanning something in a public place.
The headers this site sets
Every response carries the block below. It is printed from the site's own header file rather than retyped, so the directives here cannot drift from the ones that are deployed.
- Referrer-Policy
- no-referrer
- X-Content-Type-Options
- nosniff
- X-Frame-Options
- DENY
- Permissions-Policy
- camera=(), geolocation=(), microphone=(), payment=(), usb=()
- Cross-Origin-Resource-Policy
- same-origin
- Cross-Origin-Opener-Policy
- same-origin
- Content-Security-Policy
- default-src 'self'; base-uri 'none'; connect-src 'self'; font-src 'self'; form-action 'none'; frame-ancestors 'none'; img-src 'self' blob: data:; manifest-src 'self'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; upgrade-insecure-requests
One difference is worth naming before you compare this against your developer tools, because you will see it there. The script-src and style-src your browser receives carry 'sha256-…' sources that the file above does not. Any page that hydrates the generator workspace is rendered with two short blocks of Astro's own island bootstrap and a one-line <style> rule, and framework output cannot be moved into a file from application code. A build step reads the finished pages, computes the SHA-256 of each of those blocks and writes them into the deployed header — which is why the source file cannot already contain them, since the hashes do not exist until the pages do. Hashing them is the stricter choice: 'unsafe-inline' would permit every inline script on every page, while a hash permits those exact bytes and nothing else.
The two directives that matter most for this product:
script-src 'self'with no'unsafe-inline'. A policy that permits arbitrary inline script permits the injection it exists to prevent. The code that sets the colour theme before the first frame — the one piece that genuinely cannot wait, or a dark-theme visitor sees a white flash — is a file,/theme.js, loaded render-blocking from the head, so'self'already covers it and no exception is needed. View the source of this page and the only<script>tag on it is that one: there is no inline script here at all. A generator page contains five<script>elements without asrc: three areapplication/ld+jsonstructured data, which the parser treats as data and never executes, and two are the island bootstrap described above, each permitted by the hash of its exact contents and by nothing broader. A post-build check recomputes the hash of every inline block in the output and stops the release on any block the policy does not already cover, so an inline script the policy has not been taught about cannot appear quietly.camera=()in the Permissions-Policy. OpenQRS generates codes and never reads them, so it never needs a camera. Denying the capability at the header level means the browser will not grant it even if a future defect asked.
Why there is no redirect or shortener
OpenQRS will not operate a redirect service, a link shortener or dynamic codes. This is an architectural refusal rather than a gap in the roadmap, and the security reasons are the strongest of them:
- Every scan would pass through a server that sees the scanner's IP address, user agent, approximate location and the time. That is a tracking system, and running one contradicts the premise of the product.
- A single compromise of that service would repoint every code ever printed with it, at once, everywhere — including codes printed years earlier by people who never returned to the site.
- Every printed code would become a permanent dependency on this operator remaining online and solvent. A printed menu outlives a hosting decision.
Reporting a vulnerability
Send it to info@zenitgroup.com.co with enough detail to reproduce it: the page URL, the browser and version, the steps, and what an attacker gains. A proof of concept helps more than a scanner's severity rating.
There is no bug-bounty programme and no monetary reward, and pretending otherwise would waste your time. Reports are read and acted on; no response time is promised. Please do not test in a way that degrades the site for other people, and do not attempt anything against Cloudflare's infrastructure, which is not this operator's to authorise.
Out of scope
A static site with no server, no accounts, no cookies and no state changes has no attack surface for entire categories of finding. The following are not defects here:
- Missing cookie flags, session fixation and cross-site request forgery. There is no session, no cookie and no state-changing request.
- Missing rate limiting or account lockout. There is no account and no authentication endpoint.
- The content of a destination someone encoded. ZENIT GROUP S.A.S. is not the operator of whatever a code points at and has no record that the code was created.
- Findings that require an already-compromised device, a malicious browser extension or a hostile operating system. Those can read any page you open, and this site claims no defence against them.
- Automated scanner output with no demonstrated impact, and version-disclosure notices.
- Denial of service, volumetric testing, and social engineering of the operator or the host.