Liking cljdoc? Tell your friends :D

Secure by Default

This is an inventory, not a claim. Each row says what a generated application does without configuration, where you change it, and which test holds it. A row with no test says so.

Measures

MeasureDefaultChange it withSinceHeld by

Security headers: CSP script-src 'self', HSTS (one year, preload), X-Frame-Options: DENY, nosniff, COOP, Referrer-Policy, Permissions-Policy

On, on every routed response. No inline script, onclick or hx-on runs.

Not a config key: build your own stack with http-security-headers and a header map

1.0.0; CSP without 'unsafe-inline'/'unsafe-eval': unreleased

security-response-headers-test, no-inline-script-in-ui-source-test

CSRF token on state-changing web routes

On in a new app’s dev and test profiles. The framework default is off, so an upgrade cannot break an app, and the boot warns while it is. An enabled check with no secret refuses to boot.

:wagoe/http {:security {:csrf {:enabled? true}}}; secret from CSRF_SECRET, else JWT_SECRET

1.0.0

csrf-interceptor-protection-test, csrf-default-is-opt-in-test, csrf-skips-a-bearer-only-web-write, a-new-app-checks-csrf-in-every-profile-it-generates

Session cookie: HttpOnly, SameSite=Strict, Secure

On. Secure unless you turn it off; the generated dev profile does, for plain HTTP.

:wagoe/settings {:secure-cookies? false}

1.0.0

login-cookie-hardening-test, login-cookie-secure-attribute-test

Session tokens stored hashed

On. The database holds a hash, never the token.

—

1.1.0

a-login-stores-the-hash-not-the-token

Password hashing

bcrypt+SHA-512 (buddy-hashers), work factor 12, for passwords and MFA backup codes.

Not a config key: the constant wagoe.user.shell.bcrypt/work-factor

1.0.0

passwords-hash-at-work-factor-12

Login lockout

On: 5 failed attempts lock the account for 15 minutes, counted atomically.

:wagoe/user {:auth {:max-failed-attempts 5 :lockout-duration-minutes 15}}

1.1.0

lockout-blocks-correct-password-after-max-attempts, parallel-attempts-stop-at-the-limit

HTTP rate limiting

On in a new app’s dev profile, off in test. Enabled in production with no :wagoe/cache active, it refuses to boot.

:wagoe/http {:rate-limit {:enabled? true :limit 100 :window-ms 60000}}

1.0.0

exceeding-limit-returns-429-shape, absent-config-is-a-noop

MFA (TOTP); turning it off needs the password or a code

Opt-in per user. The secret is encrypted at rest, backup codes are hashed, wrong proofs count toward the lockout.

A :mfa-required? hook to make it mandatory

1.1.0

disable-mfa-without-proof-is-refused, wrong-disable-proofs-count-towards-the-lockout

Login response carries no secret

No password hash, MFA secret or backup code in any login response.

—

1.1.0

a-login-response-carries-no-secret

JWT secret

A missing secret, or one under 32 characters, refuses to boot. The algorithm is pinned to HS256.

JWT_SECRET

1.0.0

jwt-secret-validation-test, jwt-algorithm-pinning-test

Every route needs a signed-in user

On, unless the route says :public true.

Route data :public true

1.0.0

every-route-refuses-anonymous-callers-unless-public

Authorization rules enforced

wagoe new writes :enforce. With the key absent the framework does not enforce. An unknown mode refuses to boot.

:wagoe/authorization {:mode :enforce}

1.1.0

enforce-applies-each-rule-in-order, an-unknown-mode-fails-boot, a-new-app-enforces-authorization

Open redirects refused

A return-to that could leave the site falls back to a local path.

—

1.0.0

local-path?-refuses-anything-a-browser-could-leave-the-site-by

HTML output escaped

Hiccup escapes every string unless you mark it raw.

—

1.0.0

hiccup-xss-escaping-test

SQL parameterised

Queries go through HoneySQL with bound parameters.

—

1.0.0

sql-parameterization-test checks the mechanism, not every query.

Errors leak no internals

A 500 answers Internal Server Error and a correlation id; the exception stays in the log.

—

1.0.0

error-500-does-not-leak-internals-test

Personal data redacted in logs and error reports

On. Sentry redacts even without a :redact option; database values are stripped from logged exceptions.

Sentry :redact

1.1.0

redact-for-log-test, a-direct-log-call-writes-no-quoted-value

Tenant isolation

A schema per tenant, on PostgreSQL only: other databases refuse to boot with tenancy on, H2 unless :allow-h2? true.

:wagoe/tenant

1.0.0

The tenancy registry: every library that carries a tenant id names its test.

Secrets outside config files

#secret and #env read secrets from the environment or a file. bb doctor fails on a literal password, secret, token or key outside the test profile, or an #or fallback to one in prod and acc.

#secret, :secret-sources

1.1.0

secrets_test, a-plaintext-secret-fails-outside-test

Known vulnerabilities in dependencies

Scanned weekly, on every change to a deps.edn, and before each release publishes.

.wagoe/check-vulns.edn to allowlist, with a reason

1.1.0; release gate: unreleased

security.yml, check_vulns_test

Tests tagged :security also run as their own required job on every code change; the others run in their library’s job.

What you do yourself

  • TLS. Wagoe sets HSTS and Secure cookies; something in front of it has to terminate HTTPS.

  • CSRF and rate limiting. Keep both on in the profile you deploy; rate limiting there needs an active :wagoe/cache.

  • Secrets. Keep them in your platform’s secret store and read them with #secret or #env.

  • Backups, OS and JVM patches, database access. Outside the framework.

What we do not claim

Wagoe has had no external audit and no penetration test. It has one maintainer.

How a fix reaches you

See Security fixes.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close