Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@etern8/secure

Механические проверки безопасности для проектов ETERN8 на Next.js.

Пакет закрывает три класса дефектов, каждый из которых доезжал до прода на боевом магазине и ни один из которых не виден ни в обзоре кода, ни в обычных тестах:

  1. Мутация админки без проверки права. Server action — публично вызываемая точка. Гейт в layout раздела закрывает экран, но не операцию: к экшену обращаются POST-ом мимо интерфейса.
  2. "use server" на файле открывает ВСЕ экспорты. Директива стоит на файле, а не на функции, поэтому «служебная» функция рядом с закрытыми обёртками тоже вызываема снаружи.
  3. Предел частоты, который есть и не работает. Если ключ считается по ЛЕВОМУ узлу X-Forwarded-For, клиент подставляет заголовок и получает новое ведро счётчика. Предел выглядит рабочим и не ограничивает ничего.

Почему проверка, а не правило в документе

Все три дефекта возникли при том, что правило было известно и записано. Первый повторялся четыре раза подряд, потому что список проверяемых файлов задавался руками и новые разделы в него не попадали. Третий появился потому, что верное правило скопировали в три места и в одной копии оно разошлось.

Отсюда устройство пакета: файлы ищутся по директиве, а не по имени, проверяется каждый экспорт, и ни один файл не может остаться без решения — либо закрыт правом, либо внесён в список с письменной причиной.

Установка

Пакет ставится прямо из git по тегу — реестр не нужен:

pnpm add -D "github:yashafake/etern8-secure#v0.1.4"

Обязательный второй шаг для pnpm 10+. Любую git-зависимость pnpm считает требующей сборки и отказывается ставить, пока её не разрешили явно. Добавьте в pnpm-workspace.yaml проекта:

allowBuilds:
  "@etern8/secure": true

Без этой записи установка падает с ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED. Отказ громкий и на этапе установки, так что незамеченным он не пройдёт.

Собранный dist лежит прямо в репозитории, поэтому у потребителя ничего не компилируется — разрешение нужно только чтобы снять запрет pnpm.

Как подключить

Анализаторы возвращают находки и ничего не бросают — тестовый движок им не нужен. Проект пишет один тест:

import { describe, expect, it } from "vitest";
import {
  analyzeActionSurface,
  analyzeHeaderTrust,
  analyzeRateLimits,
  collectSources,
  formatFindings,
} from "@etern8/secure";

const files = collectSources({
  root: process.cwd(),
  roots: ["lib", "app", "components"],
});

describe("стандарт безопасности", () => {
  it("у каждой админской мутации есть право", () => {
    const findings = analyzeActionSurface(files, {
      isAdminFile: ({ path, source }) =>
        path.includes("(admin)") ||
        /(^|\/)lib\/admin\//.test(path) ||
        path.endsWith("admin-actions.ts") ||
        /@\/lib\/admin\/(guard|rbac)/.test(source),
      checksRight: (body) => /requireAction\(|requireSection\(/.test(body),
      customerFacing: {
        "lib/wishlist/actions.ts": "своё избранное под сессией",
      },
    });
    expect(findings, formatFindings(findings)).toEqual([]);
  });

  it("адрес клиента берётся по одному правилу", () => {
    const findings = analyzeHeaderTrust(files, { ipRuleHome: "lib/rate-limit.ts" });
    expect(findings, formatFindings(findings)).toEqual([]);
  });

  it("точки без входа ограничены по частоте", () => {
    const findings = analyzeRateLimits(files, {
      publicActionFiles: ["lib/auth/actions.ts", "lib/contact/actions.ts"],
    });
    expect(findings, formatFindings(findings)).toEqual([]);
  });
});

Помощник времени выполнения

import { clientIpFromHeaders } from "@etern8/secure/runtime";

// в server action
const ip = clientIpFromHeaders(await headers());
// в route handler
const ip = clientIpFromRequest(request);

Правило: берём X-Real-IP, иначе правый узел X-Forwarded-For — тот, что дописал наш прокси. Держать это одной функцией важнее, чем кажется: именно размножение трёх строк дало обход предела на входе в админку.

Условие применимости. Правило верно, пока запрос ВСЕГДА проходит через ваш прокси и тот дописывает узел. Если приложение доступно напрямую, минуя прокси, правый узел так же подделен, как левый. Закрыть прямой доступ — часть стандарта, а не деталь развёртывания.

Что настраивается

Проверка Обязательное Полезное
analyzeActionSurface isAdminFile, checksRight customerFacing, readOnlyExports, guardImportPattern
analyzeHeaderTrust ipRuleHome
analyzeRateLimits publicActionFiles sessionBounded, hasRateLimit

Списки исключений намеренно требуют причину строкой, а не голый путь: без неё список за пару месяцев превращается в способ обойти правило.

guardImportPattern — осторожно

По умолчанию за обёртку-гейт считаются ввезённые имена вида guard*. Не записывайте сюда функции типа hasAction, которые возвращают булево, чтобы спрятать кнопку: они ничего не запрещают, и их зачёт превратит проверку в пропускающую ровно тот дефект, ради которого она стоит.

Известные границы

Разбор текстовый, без AST — сознательно: проверка обязана гоняться в любом проекте без сборки, плагинов и совпадения версий парсера, иначе её не подключат. Цена — слепые зоны, и они закрыты запретами, а не умолчанием:

  • экшен, объявленный стрелкой в export const, разбор не видит → такая форма запрещена отдельной находкой action.arrow-export;
  • гейт засчитывается по вызову имени, поэтому обёртку, которая право не проверяет, но названа guardX, пакет засчитает. Имена обёрток — часть ревью, а не проверки.

Пакет проверяет наличие права и предела, но не их правильность: что роль подобрана верно и что предел не задран — вопрос обзора кода.

Проверено на живом проекте

Анализаторы прогнаны по дереву боевого магазина в состоянии до починок и независимо переоткрыли все дефекты, найденные тогда вручную за два подхода: три незакрытых экспорта в одном файле с директивой, взятие левого узла X-Forwarded-For в проверке входа, скопированное правило извлечения адреса и четыре публичные точки без ограничения частоты — десять находок.

На том же дереве после починок — ноль находок.

Это и есть смысл пакета: два подхода ручного аудита он воспроизводит за один прогон и делает это на каждом коммите.

Разработка

pnpm install
pnpm test
pnpm typecheck
pnpm build

Порядок выпуска

dist лежит в репозитории, поэтому его легко забыть пересобрать — тогда потребители получат старый код под новым тегом. Порядок такой:

pnpm test && pnpm typecheck
pnpm build                 # обязательно ДО коммита
# поднять "version" в package.json
git add -A && git commit
git tag -a vX.Y.Z -m "" && git push origin main --tags

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages