Skip to content
LexerLangv0.1.0

Middleware

security_headers, use_cors, rate_limit, trust_proxy and CSRF.

Written once at the application level and applied to every request:

LexerLang
with $app
    .trust_proxy(true)
    .security_headers({content_security_policy: "default-src 'self'"})
    .use_cors({origins: ["https://example.com"], credentials: true})
    .rate_limit({limit: 60, window: 60, redis: $redis})
end with

Security headers#

LexerLang
security.headers({
    content_security_policy: "default-src 'self'",
    frame_options: "SAMEORIGIN",
    hsts_days: 180
})

Written by default:

Text
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin

CSP and HSTS are not written unless asked for. The right CSP depends on the application; an invented value either breaks the page or protects nothing. HSTS makes a site that has not moved to HTTPS unreachable in the browser — and undoing it means waiting for the max-age to expire.

trust_proxy#

LexerLang
$app.trust_proxy(true)

By default $req.ip does not read the proxy header (X-Forwarded-For). If it did, a client could invent its own address and every IP-based limit — including the rate limit — would become meaningless.

Turn trust_proxy(true) on only when you really are behind a reverse proxy; then it is your own nginx writing that header.

Rate limiting#

LexerLang
$app.rate_limit({limit: 60, window: 60, redis: $redis})

Sixty requests a minute. The limit runs before routing; once it is exceeded the handler does not run at all.

The counter is per IP. Without redis the counter is kept in memory — enough for an application running as one process, but with several processes each keeps its own counter and the real limit multiplies.

CORS#

LexerLang
$app.use_cors({origins: ["https://example.com"], credentials: true})

credentials: true cannot be combined with origins: ["*"]; browsers already reject that combination. For requests carrying credentials you have to name the allowed origins.

CSRF#

The token lives in the session and does not change for its duration — if it were renewed on every request, two tabs open at the same time would invalidate each other's token.

LexerLang
# in the form
$doc.body.add(`
    <form method="POST" action="/submit">
        ` + security.csrf.field() + `
        <button>Submit</button>
    </form>
`)
LexerLang
# in the handler
if not security.csrf.check($req) then
    $res.status(403)
    $res.send("invalid request")
    return
end if

check looks at the _csrf form field first and the X-CSRF-Token header second: form submissions use the first, requests made from the browser the second. The comparison is constant-time.

CallWhere it goes
security.csrf.token()the raw value
security.csrf.field()a hidden field inside a form
security.csrf.meta()a meta tag in the document head
security.csrf.check($req)verification

Rich text#

sanitize_html is only for places where you really do have to store HTML — the output of a rich text editor, for instance.

LexerLang
$doc.body.add(security.sanitize_html($post.body))

It works from an allow list: p b strong em i u s a img ul ol li h1-h4 code pre blockquote hr br. Every other tag becomes text and every attribute is dropped. script and style are removed along with their contents. href/src accept only http, https, mailto, tel and relative addresses — javascript: and data: are both ways to run code.

On the SQL side#

There is no escaping function for SQL: values already travel as parameters. The only place parameters do not cover is table and column names:

LexerLang
$column = security.identifier($req.get("sort"), ["name", "price", "stock"])
$db.query("SELECT * FROM products ORDER BY " + $column)

identifier does not escape; it picks from an allow list. With no match it returns the first item, so the caller always gets a valid name.

LexerLang
$db.query("SELECT * FROM products WHERE name LIKE ?",
          "%" + security.escape_like($search) + "%")

escape_like escapes % and _. That is not an injection, but a % typed by the user would return every record.

Next#

Production.