{"id":35402627,"url":"https://github.com/codemonster-ru/security","last_synced_at":"2026-01-13T19:41:18.856Z","repository":{"id":330851058,"uuid":"1117620349","full_name":"codemonster-ru/security","owner":"codemonster-ru","description":"Security components for Annabel ecosystem","archived":false,"fork":false,"pushed_at":"2026-01-02T09:24:06.000Z","size":29,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-08T09:15:56.575Z","etag":null,"topics":["annabel","codemonster","csrf","php","rate-limiting","security","throttle"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codemonster-ru.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-16T15:15:35.000Z","updated_at":"2026-01-02T09:24:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/codemonster-ru/security","commit_stats":null,"previous_names":["codemonster-ru/security"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/codemonster-ru/security","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codemonster-ru%2Fsecurity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codemonster-ru%2Fsecurity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codemonster-ru%2Fsecurity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codemonster-ru%2Fsecurity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codemonster-ru","download_url":"https://codeload.github.com/codemonster-ru/security/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codemonster-ru%2Fsecurity/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28397826,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["annabel","codemonster","csrf","php","rate-limiting","security","throttle"],"created_at":"2026-01-02T11:51:20.734Z","updated_at":"2026-01-13T19:41:18.851Z","avatar_url":"https://github.com/codemonster-ru.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# codemonster-ru/security\n\n[![Latest Version on Packagist](https://img.shields.io/packagist/v/codemonster-ru/security.svg?style=flat-square)](https://packagist.org/packages/codemonster-ru/security)\n[![Total Downloads](https://img.shields.io/packagist/dt/codemonster-ru/security.svg?style=flat-square)](https://packagist.org/packages/codemonster-ru/security)\n[![License](https://img.shields.io/packagist/l/codemonster-ru/security.svg?style=flat-square)](https://packagist.org/packages/codemonster-ru/security)\n[![Tests](https://github.com/codemonster-ru/security/actions/workflows/tests.yml/badge.svg)](https://github.com/codemonster-ru/security/actions/workflows/tests.yml)\n\n`codemonster-ru/security` is a set of reusable security components for the Annabel ecosystem:\n\n-   CSRF protection (`VerifyCsrfToken`) with a token from POST (`_token`) and/or headers (`X-CSRF-TOKEN`, `X-XSRF-TOKEN`)\n-   Rate limiting / brute-force protection (`ThrottleRequests`) with a configurable key and storage layer\n\nNo Laravel/Symfony dependencies. Compatible with `codemonster-ru/http` and `codemonster-ru/session`.\n\n## Installation\n\n```bash\ncomposer require codemonster-ru/security\n```\n\nFor monorepo development, you can use a `path` repository (as in `annabel-skeleton/composer.local.json`).\n\n## Quick Start (Annabel)\n\nAnnabel loads providers from `bootstrap/providers/*.php`.\n\n1. Add a provider:\n\n`bootstrap/providers/SecurityServiceProvider.php`\n\n```php\n\u003c?php\n\nnamespace App\\Providers;\n\nuse Codemonster\\Security\\Providers\\SecurityServiceProvider as BaseSecurityServiceProvider;\n\nclass SecurityServiceProvider extends BaseSecurityServiceProvider {}\n```\n\n2. Add config:\n\n`config/security.php`\n\n```php\n\u003c?php\n\nreturn [\n    'csrf' =\u003e [\n        'enabled' =\u003e true,\n        'add_to_kernel' =\u003e true,\n        'verify_json' =\u003e false,\n        'input_key' =\u003e '_token',\n        'except_methods' =\u003e ['GET', 'HEAD', 'OPTIONS'],\n        'except' =\u003e ['api/*'],\n    ],\n    'throttle' =\u003e [\n        'enabled' =\u003e true,\n        'add_to_kernel' =\u003e false,\n        'max_attempts' =\u003e 60,\n        'decay_seconds' =\u003e 60,\n        'storage' =\u003e 'session', // session | database | redis\n        'connection' =\u003e null, // database connection name\n        'table' =\u003e 'throttle_requests',\n        'redis' =\u003e null, // Redis client instance or container id/class\n        'prefix' =\u003e 'throttle:',\n        'presets' =\u003e [\n            'login' =\u003e [\n                'ip' =\u003e ['max_attempts' =\u003e 60, 'decay_seconds' =\u003e 60],\n                'account' =\u003e [\n                    'max_attempts' =\u003e 5,\n                    'decay_seconds' =\u003e 60,\n                    'field' =\u003e 'email',\n                ],\n            ],\n            'api' =\u003e ['max_attempts' =\u003e 120, 'decay_seconds' =\u003e 60],\n        ],\n        'except' =\u003e [],\n        'trusted_proxies' =\u003e ['10.0.0.0/8'],\n    ],\n];\n```\n\nBy default, CSRF is enabled globally (via `Kernel::addMiddleware`), but throttling is not (so as not to surprise all routes).\n\n## CSRF\n\n### How is it checked?\n\n`Codemonster\\Security\\Csrf\\VerifyCsrfToken`:\n\n-   Skips methods from `except_methods` (`GET/HEAD/OPTIONS` by default)\n-   By default, **does not validate JSON requests** (if `Accept: application/json`) to avoid breaking the API\n-   Validates the token:\n    -   In the body: `_token` (configured via `input_key`)\n    -   Or in the headers: `X-CSRF-TOKEN`, `X-XSRF-TOKEN`\n-   On error, returns `419` (`application/json` or `text/plain`)\n\nSecurity note: if your API uses cookies or other stateful auth, enable `verify_json` to protect JSON POST/PUT/PATCH/DELETE requests too.\n\n### Helpers\n\nThe package autoloads helpers:\n\n-   `csrf_token(): string`\n-   `csrf_field(): string` - ready-to-use `\u003cinput type=\"hidden\" name=\"_token\" ...\u003e`\n\nExample in the form:\n\n```php\necho '\u003cform method=\"POST\" action=\"/submit\"\u003e';\necho csrf_field();\necho '\u003cbutton type=\"submit\"\u003eOK\u003c/button\u003e';\necho '\u003c/form\u003e';\n```\n\n## Throttle / Rate limiting\n\n`Codemonster\\Security\\RateLimiting\\ThrottleRequests`:\n\n-   stores the attempt counter in storage via `ThrottleStorageInterface`\n-   the package contains at least one implementation: `SessionThrottleStorage` (without a database)\n-   for shared storages, prefer atomic increments (implement `AtomicThrottleStorageInterface`) to avoid race conditions\n-   returns `429` + headers:\n    -   `Retry-After` (seconds)\n    -   `X-RateLimit-Limit`\n    -   `X-RateLimit-Remaining`\n    -   `RateLimit-Limit`\n    -   `RateLimit-Remaining`\n    -   `RateLimit-Reset` (unix timestamp)\n\n### Best practices\n\n-   Enable `verify_json` for stateful APIs (cookies, sessions) to avoid CSRF bypasses.\n-   Configure `trusted_proxies` when running behind a proxy; otherwise `X-Forwarded-For` should be ignored.\n-   Use database or Redis storage in multi-node deployments to avoid per-node limits.\n\n### Trusted proxies\n\nIf your app sits behind a reverse proxy or load balancer, configure `trusted_proxies` so the middleware can safely use `X-Forwarded-For` or `X-Real-IP`.\nWhen `trusted_proxies` is empty, only `REMOTE_ADDR` is trusted. Do not trust these headers unless the proxy is under your control.\n\n### Database storage (MySQL)\n\nIf you want atomic throttling across multiple nodes, use the database storage:\n\n```php\n'throttle' =\u003e [\n    'storage' =\u003e 'database',\n    'connection' =\u003e null,\n    'table' =\u003e 'throttle_requests',\n],\n```\n\nRegister the package migrations path (Annabel):\n\n```php\n// config/database.php\nreturn [\n    'migrations' =\u003e [\n        'paths' =\u003e [\n            base_path('database/migrations'),\n            base_path('vendor/codemonster-ru/security/migrations'),\n        ],\n    ],\n];\n```\n\nWithout Annabel/Database:\n\n1. Copy migrations from `vendor/codemonster-ru/security/migrations` into your project migrations directory.\n2. Run your migrations as usual.\n\nCustom table name example:\n\n```php\n'throttle' =\u003e [\n    'storage' =\u003e 'database',\n    'table' =\u003e 'app_rate_limits',\n],\n```\n\nNote: the bundled migration reads `security.throttle.table` to decide which table to create.\n\nIf you don't use migrations, create the table manually (adjust name if needed):\n\n```sql\nCREATE TABLE `throttle_requests` (\n  `key` VARCHAR(191) NOT NULL,\n  `attempts` INT NOT NULL DEFAULT 0,\n  `expires_at` INT NOT NULL DEFAULT 0,\n  PRIMARY KEY (`key`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n```\n\n### Redis storage\n\nProvide a Redis client and set storage to `redis`:\n\n```php\n'throttle' =\u003e [\n    'storage' =\u003e 'redis',\n    'redis' =\u003e Redis::class, // container id/class or instance\n    'prefix' =\u003e 'throttle:',\n],\n```\n\n### Connection to the route\n\nRouter/Kernel in Annabel supports route-level middleware:\n\n```php\nuse Codemonster\\Security\\RateLimiting\\ThrottleRequests;\n\n$app-\u003epost('/login', fn($req) =\u003e 'ok')\n    -\u003emiddleware(ThrottleRequests::class, '5,60'); // 5 attempts in 60 seconds\n```\n\nPreset example:\n\n```php\n$app-\u003epost('/login', fn($req) =\u003e 'ok')\n    -\u003emiddleware(ThrottleRequests::class, 'login');\n```\n\n### Restriction key\n\nBy default, the key is built from `ip|method|path` and hashed (`sha1`).\n\nYou can pass a callable instead of a role string:\n\n```php\nuse Codemonster\\Security\\RateLimiting\\ThrottleRequests;\n\n$app-\u003epost('/login', fn($req) =\u003e 'ok')\n    -\u003emiddleware(ThrottleRequests::class, function ($req) {\n        return 'login:' . ($req-\u003einput('email') ?? 'guest') . '|' . $req-\u003eip();\n    });\n```\n\n## Tests\n\n```bash\ncomposer test\n```\n\nOptional E2E env (tests are skipped if not set):\n\n-   MySQL: `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_DATABASE`, `MYSQL_USERNAME`, `MYSQL_PASSWORD`\n-   Redis: `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`, `REDIS_DB`\n\n## Author\n\n[**Kirill Kolesnikov**](https://github.com/KolesnikovKirill)\n\n## License\n\n[MIT](https://github.com/codemonster-ru/security/blob/main/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodemonster-ru%2Fsecurity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodemonster-ru%2Fsecurity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodemonster-ru%2Fsecurity/lists"}