Security
The security group protects HTTP routes before and after handlers run.
It covers browser security, cross-origin access, CSRF protection, IP filtering, and rate limiting.
Authentication answers:
Who is making the request?Security answers:
Should this HTTP request be allowed to reach this route?
Should this response include safer browser headers?
Should this client be slowed down or blocked?The security middleware lives under:
namespace vix::middleware::securityWhen using vix::App, prefer the App helpers:
namespace vix::middleware::appWhat security provides
The security group includes:
| Middleware | Purpose |
|---|---|
headers() | Add browser security headers to responses |
cors() | Control cross-origin browser access |
csrf() | Protect unsafe methods with a cookie/header token check |
ip_filter() | Allow or deny requests based on client IP headers |
rate_limit() | Limit request frequency per client key |
For normal vix::App applications, use the App presets:
middleware::app::security_headers_dev()
middleware::app::cors_dev(...)
middleware::app::csrf_dev(...)
middleware::app::ip_filter_dev(...)
middleware::app::ip_filter_allow_deny_dev(...)
middleware::app::rate_limit_dev(...)
middleware::app::rate_limit_custom_dev(...)Basic security setup
A small API can start with:
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::cors_dev({"https://example.com"}));
app.use("/api", middleware::app::rate_limit_dev());
app.get("/api/health", [](Request &, Response &res)
{
res.json({
"ok", true
});
});
app.run(8080);
}This gives /api a basic HTTP security layer:
security headers
make browser responses safer
CORS
controls which browser origins can call the API
rate limit
slows down abusive clientsRecommended order
A practical order is:
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::cors_dev({"https://example.com"}));
app.use("/api", middleware::app::rate_limit_dev());
app.use("/api", middleware::app::body_limit_write_dev(1024 * 1024));
app.use("/api/private", middleware::app::api_key_dev("secret"));
app.use("/api/forms", middleware::app::csrf_dev());The idea is:
security headers
can be added to most responses
CORS
must handle browser preflight requests early
rate limit
should reject abusive clients before expensive work
body limit
should reject large bodies before parsers
authentication
should protect private routes
CSRF
should protect unsafe browser form/session routesThe exact order depends on the application.
The principle is stable:
reject invalid requests early
keep route handlers focused
add response hardening consistentlySecurity headers
headers() adds HTTP response headers that improve browser security.
The App preset is:
app.use("/api", middleware::app::security_headers_dev());Example:
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::security_headers_dev());
app.get("/api/ping", [](Request &, Response &res)
{
res.json({
"ok", true,
"message", "headers applied"
});
});
app.get("/", [](Request &, Response &res)
{
res.text("public route");
});
app.run(8080);
}Test:
curl -i http://127.0.0.1:8080/api/pingTypical headers can include:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Permissions-Policy: ...Security headers usually run after the handler because they modify the final response.
request
-> security headers middleware
-> handler
-> add headers
-> responseConfigure security headers
Use the lower-level middleware when you need explicit options.
vix::middleware::security::SecurityHeadersOptions opt;
opt.x_content_type_options = true;
opt.x_frame_options = true;
opt.referrer_policy = true;
opt.permissions_policy = true;
opt.hsts = false;
opt.content_security_policy = "default-src 'self'";
app.use("/api", vix::middleware::app::adapt_ctx(
vix::middleware::security::headers(opt)
));Main options:
| Option | Purpose |
|---|---|
x_content_type_options | Add X-Content-Type-Options: nosniff |
x_frame_options | Add X-Frame-Options: DENY |
referrer_policy | Add Referrer-Policy |
permissions_policy | Add Permissions-Policy |
hsts | Add Strict-Transport-Security |
content_security_policy | Add a custom CSP value |
Only enable HSTS when the application is served through HTTPS.
If Nginx or another proxy terminates TLS, make sure the deployment is truly HTTPS-only before enabling HSTS.
CORS
CORS controls which browser origins can call your API.
It matters when a frontend is served from a different origin than the backend.
Example:
frontend
https://example.com
backend
http://127.0.0.1:8080Install CORS on your API prefix:
app.use("/api", middleware::app::cors_dev({"https://example.com"}));Example:
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::cors_dev({"https://example.com"}));
app.get("/api", [](Request &, Response &res)
{
res.json({
"ok", true
});
});
app.run(8080);
}Test:
curl -i \
http://127.0.0.1:8080/api \
-H "Origin: https://example.com"Expected response headers include:
Access-Control-Allow-Origin: https://example.com
Vary: OriginCORS preflight
Browsers send preflight requests for some cross-origin requests.
A preflight request uses:
OPTIONS
Origin
Access-Control-Request-Method
Access-Control-Request-HeadersExample:
curl -i \
-X OPTIONS http://127.0.0.1:8080/api/update \
-H "Origin: https://example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, X-CSRF-Token"Allowed origins should receive a successful preflight response.
Blocked origins should receive an error.
curl -i \
-X OPTIONS http://127.0.0.1:8080/api/update \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: POST"Expected blocked status:
403 ForbiddenCommon error code:
cors_forbiddenExplicit OPTIONS routes
For browser APIs, it is often useful to define explicit OPTIONS routes for endpoints that need preflight.
app.options("/api/update", [](Request &, Response &res)
{
res.status(204).send();
});With CORS installed on /api, the CORS middleware can handle the preflight behavior.
Example:
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::cors_dev({"https://example.com"}));
app.options("/api/update", [](Request &, Response &res)
{
res.status(204).send();
});
app.post("/api/update", [](Request &, Response &res)
{
res.json({
"ok", true
});
});
app.run(8080);
}Use explicit OPTIONS routes when you want predictable browser preflight behavior.
Configure CORS
Use the lower-level middleware when you need explicit options.
vix::middleware::security::CorsOptions opt;
opt.allowed_origins = {"https://example.com"};
opt.allow_any_origin = false;
opt.allow_credentials = true;
opt.allow_methods = {"GET", "POST", "OPTIONS"};
opt.allow_headers = {"Content-Type", "Authorization", "X-CSRF-Token"};
opt.expose_headers = {"X-Request-Id"};
opt.max_age_seconds = 600;
opt.vary_origin = true;
app.use("/api", vix::middleware::app::adapt_ctx(
vix::middleware::security::cors(opt)
));Main options:
| Option | Purpose |
|---|---|
allowed_origins | List of accepted origins |
allow_any_origin | Allow any origin when configured |
allow_credentials | Add Access-Control-Allow-Credentials |
allow_methods | Methods accepted during preflight |
allow_headers | Headers accepted during preflight |
expose_headers | Response headers visible to browsers |
max_age_seconds | Browser preflight cache duration |
vary_origin | Add Vary: Origin |
If credentials are enabled, avoid using * as the final Access-Control-Allow-Origin value.
CSRF
csrf() protects unsafe HTTP methods using the double-submit cookie pattern.
The client must send the same token in:
a cookie
a request headerDefault names are usually:
cookie: csrf_token
header: x-csrf-tokenCSRF is most useful for browser-based applications that use cookies or sessions.
Example:
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::csrf_dev());
app.get("/api/csrf", [](Request &, Response &res)
{
res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax");
res.json({
"csrf_token", "abc"
});
});
app.post("/api/update", [](Request &, Response &res)
{
res.json({
"ok", true,
"message", "CSRF passed"
});
});
app.run(8080);
}Get the cookie:
curl -i \
-c cookies.txt \
http://127.0.0.1:8080/api/csrfFail without the header:
curl -i \
-b cookies.txt \
-X POST http://127.0.0.1:8080/api/update \
-d "x=1"Expected status:
403 ForbiddenPass with matching cookie and header:
curl -i \
-b cookies.txt \
-X POST http://127.0.0.1:8080/api/update \
-H "x-csrf-token: abc" \
-d "x=1"Expected status:
200 OKCommon error code:
csrf_failedCSRF with CORS
When CORS and CSRF are used together, install CORS before CSRF.
app.use("/api", middleware::app::cors_dev({"https://example.com"}));
app.use("/api", middleware::app::csrf_dev("csrf_token", "x-csrf-token", false));CORS must be able to handle preflight requests.
CSRF should protect unsafe methods such as:
POST
PUT
PATCH
DELETEExample:
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::cors_dev({"https://example.com"}));
app.use("/api", middleware::app::csrf_dev("csrf_token", "x-csrf-token", false));
app.options("/api/update", [](Request &, Response &res)
{
res.status(204).send();
});
app.get("/api/csrf", [](Request &, Response &res)
{
res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax");
res.json({
"csrf_token", "abc"
});
});
app.post("/api/update", [](Request &, Response &res)
{
res.json({
"ok", true
});
});
app.run(8080);
}For cross-site cookies in browsers, production deployments may need cookie attributes such as:
SameSite=None
SecureUse those only when serving through HTTPS.
Configure CSRF
Use the lower-level middleware when you need explicit options.
vix::middleware::security::CsrfOptions opt;
opt.cookie_name = "csrf_token";
opt.header_name = "x-csrf-token";
opt.protect_get = false;
app.use("/api", vix::middleware::app::adapt_ctx(
vix::middleware::security::csrf(opt)
));Main options:
| Option | Purpose |
|---|---|
cookie_name | Cookie that contains the CSRF token |
header_name | Header expected to contain the CSRF token |
protect_get | Require CSRF on GET when set to true |
Usually keep protect_get false.
GET routes should normally be safe and side-effect free.
IP filter
ip_filter() allows or denies requests based on a client IP extracted from headers.
Common headers are:
x-forwarded-for
x-real-ipThis is useful behind reverse proxies, internal APIs, admin endpoints, and private dashboards.
Example:
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::ip_filter_allow_deny_dev(
"x-forwarded-for",
{"10.0.0.1", "127.0.0.1"},
{"9.9.9.9"},
true
));
app.get("/", [](Request &, Response &res)
{
res.text("public route");
});
app.get("/api/hello", [](Request &req, Response &res)
{
res.json({
"ok", true,
"x_forwarded_for", req.header("x-forwarded-for")
});
});
app.run(8080);
}Allowed IP:
curl -i \
http://127.0.0.1:8080/api/hello \
-H "X-Forwarded-For: 10.0.0.1"Blocked by allow list:
curl -i \
http://127.0.0.1:8080/api/hello \
-H "X-Forwarded-For: 1.2.3.4"Explicitly denied:
curl -i \
http://127.0.0.1:8080/api/hello \
-H "X-Forwarded-For: 9.9.9.9"Expected blocked status:
403 ForbiddenCommon error codes:
ip_denied
ip_not_allowedConfigure IP filter
Use the lower-level middleware when you need explicit control.
vix::middleware::security::IpFilterOptions opt;
opt.header_name = "x-forwarded-for";
opt.allow = {"10.0.0.1", "127.0.0.1"};
opt.deny = {"9.9.9.9"};
opt.use_remote_addr_fallback = true;
app.use("/api", vix::middleware::app::adapt_ctx(
vix::middleware::security::ip_filter(opt)
));Main options:
| Option | Purpose |
|---|---|
allow | If non-empty, only listed IPs are allowed |
deny | Denied IPs are rejected before allow rules |
header_name | Header used to extract the client IP |
use_remote_addr_fallback | Try fallback headers such as x-real-ip |
Deny rules win before allow rules.
Be careful with proxy headers
Headers such as X-Forwarded-For can be spoofed if clients connect directly to your server.
Use IP filtering with proxy headers only when:
your app is behind a trusted proxy
the proxy overwrites client-provided forwarding headers
direct public access to the app port is blockedIf your app is exposed directly to the internet, do not blindly trust X-Forwarded-For.
Rate limiting
rate_limit() limits how often a client can call a route.
It uses a token bucket model.
A client key is usually derived from a header such as:
x-forwarded-for
x-real-ipThe App preset is:
app.use("/api", middleware::app::rate_limit_dev());For demos, use a small capacity and no refill:
app.use("/api", middleware::app::rate_limit_custom_dev(
5.0,
0.0
));Example:
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::rate_limit_custom_dev(
5.0,
0.0
));
app.get("/", [](Request &, Response &res)
{
res.text("public route");
});
app.get("/api/ping", [](Request &req, Response &res)
{
res.json({
"ok", true,
"msg", "pong",
"xff", req.header("x-forwarded-for")
});
});
app.run(8080);
}Test:
for i in $(seq 1 6); do
echo "---- $i"
curl -i http://127.0.0.1:8080/api/ping
doneThe sixth request can return:
429 Too Many RequestsCommon error code:
rate_limitedCommon response headers:
X-RateLimit-Limit
X-RateLimit-Remaining
Retry-After
X-RateLimit-ResetConfigure rate limit
Use the lower-level middleware when you need exact behavior.
vix::middleware::security::RateLimitOptions opt;
opt.capacity = 60.0;
opt.refill_per_sec = 1.0;
opt.add_headers = true;
opt.key_header = "x-forwarded-for";
app.use("/api", vix::middleware::app::adapt_ctx(
vix::middleware::security::rate_limit(opt)
));Main options:
| Option | Purpose |
|---|---|
capacity | Maximum burst size |
refill_per_sec | Tokens added per second |
add_headers | Add rate limit headers |
key_header | Header used to derive the default client key |
key_fn | Custom function used to derive the client key |
Use key_fn when the key should come from something else, such as a tenant id, authenticated subject, or custom header.
vix::middleware::security::RateLimitOptions opt;
opt.capacity = 100.0;
opt.refill_per_sec = 10.0;
opt.key_fn = [](const vix::middleware::Request &req)
{
const std::string tenant = req.header("x-tenant-id");
return tenant.empty() ? "anonymous" : tenant;
};
app.use("/api", vix::middleware::app::adapt_ctx(
vix::middleware::security::rate_limit(opt)
));Combine CORS, IP filter, and rate limit
A realistic API may combine multiple security layers.
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::cors_dev({"http://localhost:5173"}));
app.use("/api", middleware::app::ip_filter_allow_deny_dev(
"x-forwarded-for",
{},
{"1.2.3.4"},
true
));
app.use("/api", middleware::app::rate_limit_custom_dev(
5.0,
0.0,
"x-forwarded-for"
));
app.options("/api/ping", [](Request &, Response &res)
{
res.status(204).send();
});
app.get("/api/ping", [](Request &req, Response &res)
{
res.json({
"ok", true,
"ip", req.header("x-forwarded-for")
});
});
app.run(8080);
}Test allowed origin:
curl -i \
http://127.0.0.1:8080/api/ping \
-H "Origin: http://localhost:5173" \
-H "X-Forwarded-For: 9.9.9.9"Test denied IP:
curl -i \
http://127.0.0.1:8080/api/ping \
-H "Origin: http://localhost:5173" \
-H "X-Forwarded-For: 1.2.3.4"Test rate limit:
for i in $(seq 1 6); do
echo "---- $i"
curl -i \
http://127.0.0.1:8080/api/ping \
-H "Origin: http://localhost:5173" \
-H "X-Forwarded-For: 9.9.9.9"
doneThis composition keeps route handlers simple.
The security layer decides whether the request should reach the route.
Complete security example
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
int main()
{
App app;
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::cors_dev({"https://example.com"}));
app.use("/api", middleware::app::rate_limit_custom_dev(10.0, 1.0));
app.use("/api/forms", middleware::app::csrf_dev("csrf_token", "x-csrf-token", false));
app.options("/api/forms/update", [](Request &, Response &res)
{
res.status(204).send();
});
app.get("/api/health", [](Request &, Response &res)
{
res.json({
"ok", true
});
});
app.get("/api/forms/csrf", [](Request &, Response &res)
{
res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax");
res.json({
"csrf_token", "abc"
});
});
app.post("/api/forms/update", [](Request &, Response &res)
{
res.json({
"ok", true,
"message", "updated"
});
});
app.run(8080);
}This gives the API:
security headers
CORS
rate limiting
CSRF on form routes
explicit preflight routeAuthentication can be added on top for private routes.
Security vs authentication
Do not put everything in one category.
Security middleware protects the HTTP surface.
CORS
CSRF
security headers
IP filter
rate limitAuthentication middleware identifies the caller.
API key
JWT
RBAC
sessions
permissionsThey work together.
Example:
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::cors_dev());
app.use("/api", middleware::app::rate_limit_dev());
app.use("/api/admin", middleware::app::api_key_dev("secret"));The security layer protects the HTTP surface.
The authentication layer protects private actions.
Summary
Use the security group to protect routes before handlers do application work.
A good starting stack is:
app.use("/api", middleware::app::security_headers_dev());
app.use("/api", middleware::app::cors_dev({"https://example.com"}));
app.use("/api", middleware::app::rate_limit_dev());Add more specific protections when needed:
app.use("/api/forms", middleware::app::csrf_dev());
app.use("/api/internal", middleware::app::ip_filter_allow_deny_dev(
"x-forwarded-for",
{"10.0.0.1"},
{},
true
));Remember the model:
security middleware decides whether the request should reach the route
security headers make responses safer
authentication is documented separately