Security

What is implemented, and what is not

Palms Links is a system-to-system API holding customer link data, API credentials and pseudonymised traffic counts. This page describes the controls that exist in the running code, the threat each one addresses, and — in the last section — the gaps. Read the last section before quoting any of this to a procurement team.

No certification is held

No ISO 27001, no SOC 2, no PCI DSS, no CSA STAR, no independent audit of any kind. The platform is designed to align with the Saudi Personal Data Protection Law and with the practices those standards describe; it has not been assessed against them by anyone. There is no such thing as "PDPL certification" and nobody should be offering you one.

1. Credentials at rest

The threat is database disclosure — SQL injection, a readable backup, a provider incident. Nothing in this table is usable by an attacker holding the database alone.

Value Storage Why
Panel passwords Argon2id m=19456, t=2, p=1 The OWASP profile shared hosting can actually sustain. Rehashed on sign-in when the parameters change.
API secrets HMAC-SHA256 under a server pepper 238 bits of entropy leaves no offline search worth slowing down, and a slow hash would be paid on every request. The pepper lives only in env.php, so a database dump cannot verify — let alone forge — a key.
TOTP secrets, webhook and signing secrets AES-256-GCM, purpose-bound They must be recoverable to compute a code or an HMAC. Each purpose derives its own subkey through HKDF-SHA256, with the purpose string bound in as additional authenticated data — a sealed TOTP secret moved into the webhook column does not decrypt there.
Recovery codes HMAC digests only A database dump does not yield a working second factor.
Link passwords Argon2id Same reasoning as panel passwords. Never returned by the API.
Visitor identifiers HMAC under a rotating pepper, truncated to 128 bits Enough to count unique visitors for a day, not enough to follow a person across periods.

2 & 3. Authentication

Control panel

  • Password, then TOTP (RFC 6238). Two-factor is required by policy; an unenrolled user is let in far enough to enrol and no further.
  • A used TOTP counter is recorded and rejected on replay, so a shoulder-surfed code cannot be reused inside its own 30-second window.
  • Ten single-use recovery codes, shown once, stored as digests. Consuming one walks the whole list rather than breaking early, so timing does not reveal position.
  • Throttling keyed on both the submitted email and the /24 or /64 source network — an address pool defeats one, and keying only on the identifier lets an attacker lock out a real user.
  • "No such account" and "wrong password" return one message in the same time; a real Argon2id verify runs against a decoy.
  • Session id regenerated on privilege elevation, bound to a coarse client fingerprint, 30-minute idle timeout, 12-hour absolute ceiling, revocable from another device.
  • The fingerprint deliberately excludes the IP address: mobile networks change it mid-session, and logging people out for walking out of wifi range teaches them to ignore security warnings.

API

Checks run in ascending cost order, so a malformed credential never reaches the database and an invalid one never reaches the rate limiter.

  1. HTTPS required in production — a credential sent in plaintext is treated as compromised.
  2. Token shape and CRC checksum: a mistyped key is rejected with zero queries.
  3. One indexed lookup returning key, client, domain, subscription and plan in a single row.
  4. Constant-time comparison of the secret digest.
  5. Key state — revoked, expired.
  6. Tenancy state — client active, domain verified, subscription live or in grace.
  7. Source restrictions — CIDR allowlist, CORS origin allowlist.
  8. Signature verification, when the key requires it.
  9. Rate limits, per minute and per day.
  10. Monthly quota: checked, then consumed.

Timing for an unknown key id is equalised against a known one, closing the enumeration oracle. A key is only ever issued against a verified domain's active subscription, and ownership verification is performed server-side (DNS TXT, a well-known file, or a meta tag) — never by trusting a form.

4–7. Application controls

Injection

PDO with emulated prepares off, so the database server parses every statement and a parameter can never be reinterpreted as SQL. No value is ever interpolated into a query. The two places an identifier must be — a dynamic ORDER BY and one stats column name — go through an allowlist and a strict pattern. Output escaping is explicit at every interpolation point in the view layer; there is no auto-escaping engine to lull anyone into forgetting. CR/LF is stripped from email and HTTP headers, closing header injection and response splitting.

Server-side request forgery

The platform fetches URLs supplied by callers, which without a guard is a request-forgery primitive. Three parts, all necessary: scheme, host and port allowlisting with private, loopback, link-local, CGNAT and reserved ranges refused in both IPv4 and IPv6 forms; DNS resolved and every answer checked, so a name resolving to one public and one private address is refused entirely; and the connection pinned to the address that was checked. Checking without pinning is a time-of-check/time-of-use bug — the name resolves safely during the check and to the metadata endpoint microseconds later. Every redirect hop is re-validated and responses are size-capped.

CSRF and clickjacking

Every state-changing panel request carries a per-session token, rotated on sign-in and verified in middleware before the controller runs, with Origin and Referer checked as a second signal. Session cookies are Secure, HttpOnly, SameSite=Lax, 48-character ids, strict mode on. Lax rather than Strict because Strict breaks the post-sign-in redirect while Lax still blocks a cross-site POST. X-Frame-Options: DENY and frame-ancestors 'none' on top.

Content Security Policy

Nonce-based, with no unsafe-inline and no CDN anywhere — which is only sustainable because the platform has no build step and ships no third-party assets at all. Every inline block carries a per-request nonce, and one that forgets simply does not run, which is the failure mode you want. Stored XSS that reaches a page still does not execute. Sent alongside: nosniff, a strict Referrer-Policy, a deny-everything Permissions-Policy, and HSTS once enabled.

8. Rate limiting and abuse

  • Sliding-window counters in the database. A naive fixed window lets a caller send twice the limit across a boundary; weighting the previous window by how much of it is still in view removes that burst without storing a row per request.
  • Per-key limits from the plan, per-network buckets for failed authentication, and per-identifier plus per-network buckets for sign-in attempts.
  • Plan quotas are separate from rate limits and are consumed only after authentication succeeds, so guessed credentials cannot burn a customer's allowance.
  • The rate limiter fails open and logs loudly: a limiter outage must not take the API down. The signature nonce store fails closed: a signed request that cannot be replay-checked is exactly the case where the caller asked for strictness.
  • Destination blocklist by host, host suffix, keyword, regular expression and CIDR, with optional Google Safe Browsing screening. A flagged link is disabled and audited.

9. Audit and monitoring

The audit log is append-only and hash-chained: each row's hash covers its own content plus the previous row's hash, so editing or deleting any historical row breaks the chain from that point and the panel's verification reports the exact id. The read and the insert are serialised on the tip row, so concurrent writers cannot fork the chain. Secrets are redacted from change payloads before the write, not on display.

Alongside it: every API call is recorded with status, error code, duration, bytes, endpoint template and a pseudonymised source address; sign-in attempts are both the throttle source and the access evidence; application logs are structured NDJSON with secrets redacted on the way in; and every scheduled task records its outcome and duration.

What a hash chain does and does not do

It addresses an insider or an attacker with database access quietly removing the record of what they did. It does not prevent that — nothing inside a database the operator controls can — it makes it detectable, which is the property an audit trail actually needs. There is no write-once storage and no external anchoring of the chain tip.

10. Privacy and data residency

How visitor data is handled

  • IP handling is set to anonymised. The default truncates before storage — last IPv4 octet, low 80 bits of IPv6 — which is the anonymisation approach regulators recognise. It can be tightened to pseudonymised-only, or to storing nothing at all.
  • Visitor identifiers are HMACs under a pepper that rotates every 30 days. Rotation severs linkage between periods, which is what prevents long-term tracking of an individual.
  • Dashboards and the statistics API read a pre-aggregated daily rollup, never raw events, so raw rows can be purged aggressively without losing history. Breakdowns are counts; they contain no individual records.
  • Personal-data columns are marked in the schema, so the retention job and any future export work from an unambiguous inventory.

Retention

Applied by the hourly maintenance job. These are the configured values, not aspirations.

Click events90 days
API request log180 days
Sign-in attempts365 days
Application logs60 days
Audit entries1825 days
Aggregated daily statisticsKept

Data-subject requests are tracked with a 30-day response target and a 72-hour breach notification target. Both are operational targets configured in the panel, not a contractual guarantee from a third party.

Hosting is outside the Kingdom

Personal data is processed on infrastructure located outside the Kingdom of Saudi Arabia. Clients are informed before onboarding and a transfer risk assessment is maintained. The current region is European Union, on Hostinger International Ltd.. This is disclosed here, in the privacy notice and in onboarding, because a cross-border transfer that a customer discovers later is a compliance problem for them, not just for us.

11. Secrets management

  • The environment file lives outside the web root at 0600, and it is a .php file rather than a .env so that a web server misconfiguration serves an empty response instead of plaintext.
  • When the panel rewrites it: validate, write a temporary file, self-check by including it, keep a backup, atomic rename, roll back on failure. One malformed value cannot take the platform offline with no way back in through the panel that caused it.
  • Three independent keys — application encryption, API secret pepper, and the privacy pseudonymisation salt. Rotating one does not invalidate the others.
  • No secret appears in the repository, in a log, in an audit payload, or in an error message that reaches a response. Database connection errors are scrubbed before they propagate.

12. What is not done

Read this before making a claim to a customer. Every item is a real limitation of the platform as it runs today.

No web application firewall

No WAF, no managed rule set, no bot-management layer, and no DDoS protection beyond what the hosting provider applies at the network edge. Every control on this page is in-application. A volumetric attack against the redirect edge will degrade it, and the mitigation available today is provider support plus, if it becomes necessary, putting a CDN in front — which is a change to the trust model, not a setting.

No HSM or KMS

Encryption keys sit in a 0600 file on a shared host. Anyone with filesystem access as that user — including the hosting provider's staff, and anyone who compromises the account — has every key. There is no hardware boundary, no envelope encryption, no key custodian and no split knowledge.

Single region, single provider, no availability target

One shared hosting account in one data centre, currently in European Union. No multi-region failover, no hot standby, and no published uptime commitment — we do not quote a number we have not measured over a meaningful period and cannot contractually stand behind. Recovery from a provider-level failure means restoring a backup onto new hosting, and that is measured in hours.

Backups are the provider's

The hosting provider's automatic backups, with the retention their plan provides. There is no independent off-provider backup, no tested restore procedure beyond the written runbook, and no encrypted export held elsewhere. Restoring loses everything written since the last snapshot.

No automated penetration testing, no bug bounty

No scheduled scans, no fuzzing harness, and no dependency-vulnerability feed — there are no third-party dependencies to feed it. The self-test suite asserts the behaviours described on this page; it is a regression harness, not an adversary.

No incident response retainer

Breach handling is a written runbook executed by whoever is available. There is no on-call rota, no forensic retainer and no legal counsel on standby. The 72-hour notification target is a setting in the panel, not a contract with anyone who guarantees it can be met.

Rate limiting is per key, not per organisation

A client holding ten keys can consume ten times the per-key rate limit. Plan quotas are per subscription and do bound the total, but the burst behaviour is per key.

No customer-managed keys; two-factor is TOTP only

Every tenant's data is encrypted under the same platform key, so a key compromise is a platform-wide compromise. Two-factor is TOTP — no WebAuthn, no hardware keys, no push approval — and TOTP is phishable in real time by a competent attacker running a proxy.

13. Reporting a vulnerability

Write to security@palmslinks.io, also published at /.well-known/security.txt. Include what you did, what happened and what you expected.

Please do not test against live customer links — ask for a test-environment key and one will be issued. There is no bug bounty. There is a commitment to read every report, respond within five working days, and credit anyone who wants credit.

Privacy questions and data-subject requests go to privacy@palmslinks.io. The controller is Palms Links; the details are in the privacy notice and the data processing agreement.