В вашем командном task API есть одна браузерная панель, один Express backend и PostgreSQL. Используйте server-side session, если другой client не требует JWT.
Это решение — первая граница. Также вам нужны средства контроля формы запроса, доступа к базе данных, browser origins, исчерпания ресурсов, заголовков и deployment credentials. Это руководство проходит по этим границам в таком порядке и завершается release gate, который можно выполнить перед выходом в production.
В этой статье
- Определите пять границ перед добавлением middleware
- Для browser-only first-party app выбирайте sessions
- JWT полезны, но только при наличии плана выхода
- Валидируйте запросы на входе в систему
- Rate limits должны соответствовать сценарию злоупотребления endpoint
- CORS должен указывать клиентов, которым вы доверяете
- Helmet — это базовый уровень, а не проверка безопасности
- Parameterized SQL не подлежит обсуждению
- Считайте environment variables минимальным уровнем
- Выполните этот чек-лист перед deploy
Определите пять границ перед добавлением middleware
Большинство руководств по безопасности Express — это списки middleware для покупки. Но каждый контроль относится к конкретной границе и сценарию отказа.
Express предоставляет routes и композицию middleware. Но вам всё равно нужно самостоятельно определить authentication, authorization, validation, rate limiting, CORS, security headers, защиту от SQL injection и обработку secrets. Средства контроля включают sessions или JWT для authentication, schema validation на границе, endpoint-specific rate limits, allowlisted CORS, заголовки Helmet, parameterized SQL, managed secrets и release checklist.
Мыслите пятью границами:
- Identity: authentication идентифицирует вызывающую сторону; authorization проверяет, может ли этот вызывающий выполнить действие.
- Input: тела запросов, query strings и route parameters требуют ограничений формы и размера.
- Data: значения базы данных должны оставаться отделёнными от синтаксиса SQL, а ownership должен проверяться.
- Browser trust: CORS определяет, какие browser origins могут вызывать credentialed routes.
- Availability and deployment: вызывающие стороны не должны потреблять неограниченные ресурсы, а production credentials не должны утекать.
Эти средства контроля усиливают друг друга. Authentication не исправит SQL injection. Validation не отзовёт украденный token. CORS предоставляет браузерам разрешение; authentication защищает route. Поэтому держите их отдельными проверками.
Возвращайте клиентам обобщённые ошибки, а диагностические подробности храните в защищённых logs. Атакующие внимательно читают сообщения об ошибках. Endpoint, сообщающий, существует ли email, уже раскрывает полезную информацию.
Для browser-only first-party app выбирайте sessions
Возьмём team task API. Browser dashboard обращается к одному server-side приложению, а POST /login устанавливает доступ к GET /tasks, POST /tasks и PATCH /tasks/:id. Server-side sessions подходят для такой схемы, поскольку приложение может инвалидировать session, когда пользователь выходит из системы или администратор отключает аккаунт.
Рекомендация sessions-first наиболее сильна для одного браузера и одного backend; ваше хранилище, cookie policy и token lifetime зависят от deployment и набора clients.
| Concern | Server-side session | JWT |
|---|---|---|
| State location | Server-side session store | Client-held token |
| Revocation | Delete or invalidate the session | Short expiry plus a server-side revocation design |
| Browser exposure | Opaque cookie with HttpOnly | Cookie or another client storage mechanism |
| Multiple instances | Requires a shared session store | Verification can occur across instances |
| Best fit | Browser app with one server-side application | Mobile, service-to-service, IoT, or federated clients |
| Main burden | Session-store availability | Expiry, refresh rotation, and logout design |
In-memory session store исчезает после перезапуска и может разделить state между production instances. Используйте shared, durable session store, когда приложение работает в нескольких instances.
Устанавливайте cookies с HttpOnly, Secure и SameSite=Lax или Strict, когда это допускают flows вашего приложения. Rolling expiration может продлевать активные sessions. Regenerate session после login, чтобы identifier, выданный до authentication, не мог стать authenticated identifier:
req.session.regenerate((err) => {
if (err) return next(err);
req.session.userId = user.id;
res.sendStatus(204);
});
HttpOnly не позволяет JavaScript читать cookie; это не делает XSS vulnerability безвредной. Injected script всё ещё может выполнять authenticated same-origin requests из браузера.
SameSite=None требуется, когда легитимный cross-site browser flow должен отправлять cookie, и требует Secure. В такой архитектуре state-changing cookie requests нуждаются в явной CSRF-защите. Lax или Strict — полезный выбор, когда он соответствует вашей client topology, но не универсальный ответ.
JWT оправдывают свою сложность, когда credentials должны передаваться mobile clients, отдельным services, IoT devices или federated systems. Authentication идентифицирует вызывающего; затем authorization middleware должен проверять ownership или role на границе route.
JWT полезны, но только при наличии плана выхода
JWT payloads — это читаемые base64url data, а не зашифрованное содержимое, поэтому не помещайте в них passwords, secrets и sensitive personal data.
Verifier может принять JWT, не читая session row. Logout тогда требует short expiry, denylist или другого server-side control. Для browser clients storage влияет на риск: XSS payload может прочитать bearer token, к которому имеет доступ JavaScript. HttpOnly cookie ограничивает чтение token, но риск XSS requests всё ещё требует исправления injection.
Используйте JWT library, которая фиксирует принимаемый algorithm во время verification и отклоняет none. Рассматривайте signing algorithm и key distribution как deployment decisions. Перед выпуском long-lived tokens задокументируйте key rotation и то, как verifiers получают заменяющий public key или secret.
Минимальный refresh design выглядит так:
login:
issue a short-lived access JWT
issue an opaque random refresh token
store only its hash, user, device, and token family
refresh:
hash the presented refresh token
find its stored record
if the token was already used:
invalidate the entire token family
reject the request
invalidate the current token
issue a new access token and refresh token
logout:
revoke the refresh token and its device session
Используйте случайный refresh token размером 256 бит, храните только его hash и rotate его после каждого exchange. Ранее использованный token может указывать на кражу. Invalidate всю family и потребуйте от пользователя пройти authentication заново.
Short access-token lifetimes уменьшают exposure. Privileged routes должны повторно проверять roles или permissions server-side, а не доверять старому claim. «Stateless» описывает verification state; это не устраняет operational work по revocation.
Валидируйте запросы на входе в систему
Пройдите один task request через границу. POST /tasks должен принимать title и необязательную ISO calendar date. PATCH /tasks/:id должен принимать только fields, которые route предназначен обновлять.
Строгая schema может обеспечить этот contract:
import { z } from "zod";
const createTaskSchema = z.object({
title: z.string().trim().min(1).max(200),
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
}).strict();
const taskParamsSchema = z.object({
id: z.coerce.number().int().positive()
});
const updateTaskSchema = z.object({
title: z.string().trim().min(1).max(200).optional(),
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional()
}).strict();
function validateTaskRequest(req, res, next) {
const body = createTaskSchema.safeParse(req.body);
const params = taskParamsSchema.safeParse(req.params);
if (!body.success || !params.success) {
return res.status(400).json({ error: "Invalid request" });
}
req.validated = {
body: body.data,
params: params.data
};
next();
}
Пример принимает YYYY-MM-DD; parse это значение осознанно перед сохранением. Отклоняйте невозможные dates, такие как 2026-02-31, и возвращайте одну согласованную validation error вместо раскрытия parser internals.
Валидируйте body, params, query и заданные limits до business logic или database access. Передавайте дальше parsed values и отбрасывайте исходный неограниченный object.
Validation отвечает на вопрос: «Имеет ли этот request корректную форму и ограниченный размер?» Output encoding вместо этого обрабатывает destination. Экранируйте task title для HTML, email, logs или любого другого destination, который его использует. Cross-site scripting почти всегда является проблемой output, переодетой в проблему input.
Это руководство использует PostgreSQL, поэтому MongoDB operator injection и parameter-pollution middleware находятся за пределами его scope. Добавляйте destination-specific defenses, когда ваше приложение действительно обрабатывает такие data flows.
Rate limits должны соответствовать сценарию злоупотребления endpoint
Один global limiter легко установить и легко настроить неправильно. Дайте каждой public surface policy, связанный с защищаемым ресурсом:
| Surface | Starting policy | Why |
|---|---|---|
POST /login | 5 attempts per 15 minutes per IP and account | Slows credential stuffing |
POST /tasks | 30 requests per minute per authenticated account | Limits automated write abuse |
GET /tasks | 120 requests per minute per authenticated account | Leaves room for dashboard refreshes |
| Public forms | Tune above normal user activity | Avoids punishing legitimate submissions |
Эти numbers — отправные точки. Измеряйте normal traffic, учитывайте reverse proxy и храните counters в shared store, когда requests могут достигать нескольких instances.
Используйте поддерживаемую limiter implementation и применяйте limits ко всем public surfaces. Сочетайте login limit с generic response Invalid credentials, чтобы endpoint не раскрывал, существует ли account.
Application rate limits уменьшают объём automated guessing. Они не поглощают denial-of-service attack сетевого масштаба. Разместите upstream protection перед API и добавьте body-size и slow-request limits, чтобы client не мог удерживать connections открытыми с помощью маленьких отложенных payloads.
Это руководство даёт вам pattern контроля; ваш traffic и deployment model определяют точные windows, shared-store topology и token lifetimes.
CORS должен указывать клиентов, которым вы доверяете
CORS предоставляет браузерам разрешение. Authentication защищает route. Настраивайте их отдельно.
Если dashboard размещён по адресу https://app.example.com, разрешите этот origin и включайте credentials только для route group, которой нужны cookies:
import cors from "cors";
const allowedOrigins = new Set([
"https://app.example.com"
]);
const browserCors = cors({
origin(origin, callback) {
// Decide explicitly whether requests without Origin should be accepted.
if (origin === "https://app.example.com") {
return callback(null, true);
}
return callback(new Error("Origin not allowed"));
},
credentials: true
});
app.use("/api/browser", browserCors, browserApiRouter);
Никогда не объединяйте wildcard origin с credentialed requests. Requests без заголовка Origin требуют явного policy decision. CORS не authenticates их, а non-browser clients могут полностью его игнорировать.
Helmet — это базовый уровень, а не проверка безопасности
Headers полезны. Их также легко принять за security review.
Register Helmet до routes и проверяйте получившиеся headers:
import helmet from "helmet";
app.use(helmet());
Для API, которое не обслуживает HTML, Content Security Policy может иметь небольшую прямую ценность. Тестируйте HSTS только тогда, когда каждый production access path использует HTTPS, а proxy корректно передаёт request scheme.
| Helmet helps with | Helmet cannot fix |
|---|---|
| Several browser security headers | Missing authorization |
| Framing and content-type protections | Unsafe SQL |
| Baseline browser policy | Leaked secrets or stolen refresh tokens |
Проверьте package documentation версии перед тем, как полагаться на default behavior, и протестируйте headers с фактическим frontend. Header baseline полезен. Безопасны ли routes? На это должны ответить tests.
Parameterized SQL не подлежит обсуждению
Для значений PostgreSQL placeholders отделяют bound value от SQL syntax. Они не parameterize identifiers или sort expressions.
Используйте их для task lookup:
const { id } = taskParamsSchema.parse(req.params);
const result = await pool.query(
`SELECT id, title, due_date
FROM tasks
WHERE id = $1 AND owner_id = $2`,
[id, req.user.id]
);
Predicate owner_id — это authorization check: он не позволяет одному authenticated user читать task другого user. Parameterization и authorization решают разные проблемы.
Sort expression изменяет query structure, поэтому сопоставляйте request value с source-controlled SQL:
const sortOrders = {
newest: "created_at DESC",
due: "due_date ASC"
};
const orderBy = sortOrders[req.query.sort] ?? sortOrders.newest;
const result = await pool.query(
`SELECT id, title, due_date
FROM tasks
WHERE owner_id = $1
ORDER BY ${orderBy}`,
[req.user.id]
);
Это остаётся безопасным только потому, что каждое object value и fallback — literals, поддерживаемые в вашем source code. Никогда не создавайте allowlist values из request input или database data.
String interpolation в SQL — review failure, даже когда value в данный момент поступает из supposedly trusted internal variable. Query builders и ORMs всё равно требуют review APIs и SQL, которые они генерируют.
Считайте environment variables минимальным уровнем
.env — полезная отправная точка. Production всё равно требует secrets-management strategy. Credential, committed to Git и позднее удалённый, всё ещё требует rotation.
Для local development:
- Добавьте
.envв.gitignoreдо первого commit. - Commit
.env.exampleс именами variables и пустыми values. - Валидируйте required variables при startup и явно завершайте работу, если одна из них отсутствует.
- Сканируйте current working tree и repository history.
Если ваш deployment запускает Node version, поддерживающую --env-file, она может загружать local environment files без дополнительного package:
node --env-file=.env server.js
Проверьте эту команду для Node version, которую вы deploy.
Для production двигайтесь вверх по этой maturity ladder:
- Удалите hardcoded credentials.
- Используйте gitignored local environment file.
- Загружайте production values из managed secret store.
- Inject их во время runtime с least-privilege identities.
- Предпочитайте short-lived credentials и OIDC, если ваша platform их поддерживает.
AWS Secrets Manager, Google Secret Manager, Azure Key Vault и Vault — примеры managed stores. Важным является operating model. Приложение получает только нужные ему credentials, и эти credentials можно rotate без пересборки source code.
Сканируйте repository, Git history, container build context и deployment configuration на значения, похожие на secrets. Pre-commit check обнаруживает следующую утечку; history scanning обнаруживает предыдущую.
Выполните этот чек-лист перед deploy
Считайте это release blockers. Документ из сорока пунктов ничего не защищает, если его никто не запускает.
Identity
- Protected routes используют shared authentication middleware, за которым следует ownership или role check.
- Sessions устанавливают
HttpOnly,Secureи architecture-appropriateSameSite; login regenerates session. - JWT verification фиксирует algorithms, а access tokens быстро истекают.
- Refresh tokens являются opaque, hashed, rotated и revoked при logout; reuse invalidates token family.
Requests and data
- Body, query и route parameters валидируются до business logic.
- Body size, request time и endpoint-specific rate limits enforced.
- Login возвращает generic failure message.
- Output экранируется для своего destination.
- SQL values используют placeholders; structural fragments поступают только из source-controlled allowlists.
- CORS указывает known origins, а credentialed responses никогда не используют
*. - Helmet запускается до routes, и его headers работают с production frontend.
Deployment
- Отсутствующая required configuration останавливает startup.
- Secrets отсутствуют в repository, Git history, build context, container image и deployment configuration.
- Exposed credentials были rotated.
- Production identities имеют только необходимые permissions.
- External errors являются generic; подробные failures отправляются в protected logs.
- Deployed dependency versions и security settings проверены по актуальной официальной документации.
Если одна проверка не пройдена, отложите deploy, назначьте owner и повторно запустите gate после исправления. Security work становится реальной, когда кто-то может заблокировать release. Дайте этому человеку clear owner и rerun date.