Skip to content

Security

Ori Pekelman edited this page May 11, 2026 · 1 revision

Tep::Security

Two filters covering the most common cross-cutting HTTP security concerns: CORS (request side) and security headers (response side).

Tep::Security::Cors

A Tep::Filter subclass that handles preflight (OPTIONS) requests and adds the right Access-Control-Allow-* headers to actual responses.

Configuration

cors = Tep::Security::Cors.new
cors.set_origin("https://app.example.com")
cors.set_allowed_verbs("GET,POST,PUT,DELETE")
cors.set_allowed_headers("Content-Type,Authorization")
cors.set_max_age(3600)

before do
  cors.before(request, response)
end

For a fully-open API (development, or genuinely public endpoints):

cors.set_origin("*")

For a per-route policy, branch on request.path in the filter block.

What it does

  • On a preflight OPTIONS request: writes the Access-Control-Allow-{Origin,Methods,Headers,Max-Age} headers and halts with 204.
  • On any other request: adds Access-Control-Allow-Origin and Vary: Origin to the response. The actual handler still runs.

Tep::Security::Headers

Default security headers, applied as an after filter. Sensible defaults out of the box; tuneable for apps that want non-defaults.

Configuration

hdr = Tep::Security::Headers.new
hdr.set_hsts(63072000)   # default; set to 0 to disable

after do
  hdr.after(request, response)
end

Headers it sets

Header Value
Strict-Transport-Security max-age=<hsts_seconds>; includeSubDomains
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
X-Permitted-Cross-Domain-Policies none

It does NOT set Content-Security-Policy — that's app-specific and varies enough that a default would do more harm than good. Write your own:

after do
  response.headers["Content-Security-Policy"] = "default-src 'self'"
  Tep::Security::Headers.new.after(request, response)
end

Cookbook

Behind a TLS-terminating proxy

hdr = Tep::Security::Headers.new
hdr.set_hsts(63072000)

before do
  if !request.ssl?    # checks X-Forwarded-Proto
    redirect "https://" + request.host + request.path
  end
end

after do
  hdr.after(request, response)
end

request.ssl? reads X-Forwarded-Proto; the proxy needs to be configured to set it. On nginx: proxy_set_header X-Forwarded-Proto $scheme;.

Disable HSTS for a local dev binary

hdr = Tep::Security::Headers.new
if ENV.fetch("APP_ENV", "production") == "development"
  hdr.set_hsts(0)
end
after do
  hdr.after(request, response)
end

Pitfalls

  • cors.before is a NOOP on missing Origin. Same-origin requests don't get the CORS header machinery — by design.
  • HSTS is cumulative. Once a browser sees it, it'll refuse plaintext for the duration; rolling back to HTTP-only requires serving max-age=0 for the same length of time, then waiting. Test in a separate hostname first.
  • Frame-Options: DENY blocks <iframe> embedding. If your app intentionally embeds itself somewhere, override to SAMEORIGIN (and consider migrating to CSP's frame-ancestors).

Clone this wiki locally