Addon development checklist
Examples are often PHP-flavored, but every recommendation applies to any language.
Application code
- Use variable binding (or prepared statements) in SQL queries
- Why: Protection against SQL Injection attacks
- Use auto-escaping template engines or libraries
- Why: Protection against Cross-Site Scripting attacks, HTML Injection etc.
- Nice to have: Strict nonce- or hash-based Content Security Policy, can use
strict-dynamic
- Translations do not contain HTML
- Why: Because in such case it is near impossible to auto-escape output
- Forms use CSRF protection
- Why: Protection against Cross-Site Request Forgery attacks
- Insert, delete, update operations use HTTP POST method
- No debug output in production code (e.g.
var_dump() in PHP)
- Why: Debug output may leak internal or sensitive info
- In PHP, check with spaze/phpstan-disallowed-calls with
disallowed-execution-calls.neon + disallowed-insecure-calls.neon + disallowed-dangerous-calls.neon
- Item ids (user id, order id etc.) use UUIDv4 or 128-bit random strings (hexdec or Base64)
- Why: Makes ids unguessable, a defense-in-depth layer against Insecure direct object references
- The first layer: every request checks that the user is authorized to access the object
- Code contains no secrets like database credentials etc.
- Why: Leaking source code should not leak secrets
- Should be checked with scanners like Gitleaks or similar
- API tokens (e.g. Shoptet OAuth access tokens) never appear in logs or URLs
- Why: A leaked token gives full access to that merchant’s shop, and the token table is the app’s crown jewels
- Incoming webhooks are verified before processing (e.g. the Shoptet webhook signature header)
- Why: Anyone can send a request to a webhook URL, the signature proves it actually came from the platform
- Sensitive function or method parameters are marked as sensitive (e.g. with the
SensitiveParameter attribute in PHP)
- Why: Exceptions or error logs should not contain any sensitive info
- URLs do not contain private info like emails, ids should be used
- Why: Access logs or browser history should not contain private info
- Nice to have: Session cookie explicitly uses
SameSite=Strict or Lax attribute
- Why: Another protection layer against Cross-Site Request Forgery attacks
- Nice to have: Avoid JWT or at least avoid manual verification
- Why: In many scenarios, JWT (JSON Web Tokens) is a footgun and/or an overkill
- Prefer regular server-side sessions, PASETO, or plain API keys, depending on the use case; if JWT is unavoidable, verify it with a maintained library
- Sensitive operations (e.g. password change) require reauth (with 2FA, or by re-entering the password)
- Why: In case someone steals the session (or steals an unlocked laptop), the damage they can do is limited
- Sensitive operations are rate-limited (e.g. password change attempts)
- Why: Limiting possible damage
- Store only the customer and order data the app actually needs, fetch the rest from the API on demand
- Why: Data you don’t store can’t leak, and GDPR requires data minimization anyway
- When a merchant uninstalls the app, delete their data and tokens
- Why: Limits the blast radius of any future breach, and GDPR requires it anyway
Server and deployment configuration
- Uploaded files (images, PDFs) are uploaded to a domain with a different origin, and a different HTTP server serving only static files (e.g. CDN)
- Why: Uploaded files like HTML can’t be used to execute JavaScript-based attacks, and uploaded script files (e.g.
.php) cannot be executed on the server
- Content type checks still mandatory
- Use HTTPS
- Why: Encrypted traffic is encrypted, so passwords, tokens, and customer data can’t be read or modified in transit
- Nice to have:
Strict-Transport-Security header
- Why: To commit to using HTTPS
- Nice to have: Highest grade in scanners like the SSL Labs Server Test, Mozilla Observatory, securityheaders.com
- Why: These scanners can prove the app is configured securely
- Database admin tools used to manage the app (e.g. Adminer, PHPMyAdmin) are not publicly accessible
- Why: Credentials can leak, e.g. via displayed errors or debug bars, and then these tools could be used to log in to the database
- The web root directory is not the same as the app root directory (or repository root)
- Why: Internal app files should not be accessible from the browser over the Internet, by design
- No debug info publicly accessible (e.g.
/phpinfo.php etc.)
- Why: It may leak internal or sensitive info
- Production shows a generic error page, detailed errors are logged server-side, never displayed (e.g.
display_errors=Off in PHP)
- Why: Displayed exceptions and stack traces leak paths, queries, and sometimes credentials
- No debug tools enabled in production (no Symfony Profiler, no Nette Tracy etc.)
Team workflow and maintenance
- Use supported versions of the language and runtime (e.g. PHP, Node.js), regularly updated
- Why: Supported and updated versions will not contain known vulnerabilities
- Use supported versions of dependencies, libraries, packages
- Why: Fixes known bugs and vulnerabilities in third-party code
- Regularly checked with
composer outdated, composer audit, npm audit, GitHub Dependabot etc.
- New versions of dependencies are adopted only after a cooldown period (e.g. 7 days), security fixes immediately
- Why: Compromised releases (supply chain attacks) are typically detected and pulled within a few days, a cooldown means you never install them
- Supported by Dependabot (on by default since July 2026), Renovate, pnpm and npm; coming in Composer 2.11
- Developers use a password manager to manage and share secrets
- Why: Passwords and secrets should be shared and managed in a way that they can’t leak
- Passwords or other credentials shared between the merchant and the partner (in either direction, via the password manager) are changed right after the handover
- Why: If the shared password later leaks, it is no longer valid
- Developers do not use production databases to develop the app and fix bugs
- Why: Accessing production databases raises the risk of sensitive data leaks
If the app will not use Shoptet auth
If possible, the app should be designed in a way that it uses Shoptet auth (OAuth or session-based).
- Passwords are stored with suitable hashing (bcrypt or Argon2id)
- Why: Cracking passwords will be near impossible if someone has access to the database
- User accounts use 2FA (required for every account, not optional)
- Why: Stolen or reused passwords are the most common account-takeover vector
- Nice to have: Passkeys (as 2FA or a sole auth credential)
- Why: Passkeys cannot be phished
- Nice to have: Compromised or weak password checks (e.g. via Have I Been Pwned)
- Why: People reuse breached or weak passwords, checking them blocks the cheapest account-takeover attacks