rubino-agent 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +115 -0
- data/.rubocop_todo.yml +955 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +97 -0
- data/CHANGELOG.md +344 -0
- data/CONTRIBUTING.md +69 -0
- data/LICENSE +21 -0
- data/README.md +200 -0
- data/Rakefile +8 -0
- data/docs/agents.md +190 -0
- data/docs/api/v1.md +414 -0
- data/docs/architecture.md +177 -0
- data/docs/commands.md +375 -0
- data/docs/configuration.md +590 -0
- data/docs/getting-started.md +143 -0
- data/docs/jobs.md +332 -0
- data/docs/mcp.md +128 -0
- data/docs/memory.md +98 -0
- data/docs/models-and-keys.md +173 -0
- data/docs/oauth-providers.md +145 -0
- data/docs/plugins.md +195 -0
- data/docs/security.md +145 -0
- data/docs/skills.md +322 -0
- data/docs/tools.md +395 -0
- data/docs/troubleshooting.md +73 -0
- data/exe/rubino +9 -0
- data/install.sh +275 -0
- data/lib/rubino/active_skill.rb +50 -0
- data/lib/rubino/agent/agent_registry.rb +120 -0
- data/lib/rubino/agent/backoff_policy.rb +116 -0
- data/lib/rubino/agent/definition.rb +128 -0
- data/lib/rubino/agent/degenerate_recovery.rb +271 -0
- data/lib/rubino/agent/fallback_chain.rb +194 -0
- data/lib/rubino/agent/iteration_budget.rb +50 -0
- data/lib/rubino/agent/loop.rb +617 -0
- data/lib/rubino/agent/model_call_runner.rb +383 -0
- data/lib/rubino/agent/prompts/build.txt +69 -0
- data/lib/rubino/agent/prompts/compaction.txt +20 -0
- data/lib/rubino/agent/prompts/explore.txt +19 -0
- data/lib/rubino/agent/prompts/general.txt +20 -0
- data/lib/rubino/agent/prompts/plan.txt +31 -0
- data/lib/rubino/agent/response_validator.rb +70 -0
- data/lib/rubino/agent/router.rb +65 -0
- data/lib/rubino/agent/runner.rb +195 -0
- data/lib/rubino/agent/tool_executor.rb +402 -0
- data/lib/rubino/agent/truncation_continuation.rb +137 -0
- data/lib/rubino/api/middleware/auth.rb +43 -0
- data/lib/rubino/api/middleware/error_handler.rb +65 -0
- data/lib/rubino/api/middleware/json_parser.rb +100 -0
- data/lib/rubino/api/middleware/observability.rb +59 -0
- data/lib/rubino/api/middleware/rate_limit.rb +136 -0
- data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
- data/lib/rubino/api/operations/files/read_operation.rb +40 -0
- data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
- data/lib/rubino/api/operations/health_operation.rb +46 -0
- data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
- data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
- data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
- data/lib/rubino/api/operations/metrics_operation.rb +18 -0
- data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
- data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
- data/lib/rubino/api/operations/models/list_operation.rb +45 -0
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
- data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
- data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
- data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
- data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
- data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
- data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
- data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
- data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
- data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
- data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
- data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
- data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
- data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
- data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
- data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
- data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
- data/lib/rubino/api/request.rb +54 -0
- data/lib/rubino/api/responses.rb +64 -0
- data/lib/rubino/api/router.rb +72 -0
- data/lib/rubino/api/schemas.rb +103 -0
- data/lib/rubino/api/server.rb +102 -0
- data/lib/rubino/api/tls.rb +108 -0
- data/lib/rubino/attachments/classification.rb +16 -0
- data/lib/rubino/attachments/classify.rb +171 -0
- data/lib/rubino/attachments/defang.rb +47 -0
- data/lib/rubino/attachments/policy.rb +36 -0
- data/lib/rubino/attachments/preamble.rb +120 -0
- data/lib/rubino/boot/encryption_key.rb +32 -0
- data/lib/rubino/cli/chat/bang_shell.rb +257 -0
- data/lib/rubino/cli/chat/completion_builder.rb +290 -0
- data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
- data/lib/rubino/cli/chat/image_inbox.rb +168 -0
- data/lib/rubino/cli/chat/session_resolver.rb +176 -0
- data/lib/rubino/cli/chat_command.rb +1674 -0
- data/lib/rubino/cli/commands.rb +250 -0
- data/lib/rubino/cli/config_command.rb +96 -0
- data/lib/rubino/cli/doctor_command.rb +251 -0
- data/lib/rubino/cli/jobs_command.rb +60 -0
- data/lib/rubino/cli/memory_command.rb +135 -0
- data/lib/rubino/cli/onboarding_wizard.rb +207 -0
- data/lib/rubino/cli/server_command.rb +139 -0
- data/lib/rubino/cli/session_command.rb +125 -0
- data/lib/rubino/cli/setup_command.rb +107 -0
- data/lib/rubino/cli/skills_command.rb +85 -0
- data/lib/rubino/cli/tools_command.rb +81 -0
- data/lib/rubino/cli/trust_gate.rb +71 -0
- data/lib/rubino/commands/built_ins.rb +46 -0
- data/lib/rubino/commands/command.rb +116 -0
- data/lib/rubino/commands/executor.rb +550 -0
- data/lib/rubino/commands/handlers/agents.rb +510 -0
- data/lib/rubino/commands/handlers/config.rb +88 -0
- data/lib/rubino/commands/handlers/help.rb +148 -0
- data/lib/rubino/commands/handlers/jobs.rb +71 -0
- data/lib/rubino/commands/handlers/mcp.rb +229 -0
- data/lib/rubino/commands/handlers/memory.rb +200 -0
- data/lib/rubino/commands/handlers/sessions.rb +207 -0
- data/lib/rubino/commands/handlers/skills.rb +195 -0
- data/lib/rubino/commands/handlers/status.rb +211 -0
- data/lib/rubino/commands/loader.rb +90 -0
- data/lib/rubino/config/configuration.rb +455 -0
- data/lib/rubino/config/defaults.rb +569 -0
- data/lib/rubino/config/loader.rb +115 -0
- data/lib/rubino/config/reasoning_prefs.rb +67 -0
- data/lib/rubino/config/writer.rb +72 -0
- data/lib/rubino/context/compressor.rb +149 -0
- data/lib/rubino/context/environment_inspector.rb +176 -0
- data/lib/rubino/context/file_discovery.rb +45 -0
- data/lib/rubino/context/message_boundary.rb +39 -0
- data/lib/rubino/context/prompt_assembler.rb +382 -0
- data/lib/rubino/context/summary_builder.rb +159 -0
- data/lib/rubino/context/token_budget.rb +68 -0
- data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
- data/lib/rubino/database/connection.rb +77 -0
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
- data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
- data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
- data/lib/rubino/database/migrator.rb +48 -0
- data/lib/rubino/documents/converters/csv.rb +79 -0
- data/lib/rubino/documents/converters/docx.rb +129 -0
- data/lib/rubino/documents/converters/html.rb +28 -0
- data/lib/rubino/documents/converters/json.rb +35 -0
- data/lib/rubino/documents/converters/pdf.rb +59 -0
- data/lib/rubino/documents/converters/plain.rb +68 -0
- data/lib/rubino/documents/converters/pptx.rb +64 -0
- data/lib/rubino/documents/converters/xlsx.rb +62 -0
- data/lib/rubino/documents/converters/xml.rb +45 -0
- data/lib/rubino/documents/html.rb +71 -0
- data/lib/rubino/documents/registry.rb +68 -0
- data/lib/rubino/documents/table.rb +63 -0
- data/lib/rubino/documents.rb +50 -0
- data/lib/rubino/errors.rb +119 -0
- data/lib/rubino/files/workspace.rb +93 -0
- data/lib/rubino/interaction/cancel_token.rb +43 -0
- data/lib/rubino/interaction/clipboard_image.rb +84 -0
- data/lib/rubino/interaction/event_bus.rb +48 -0
- data/lib/rubino/interaction/events.rb +101 -0
- data/lib/rubino/interaction/image_input.rb +127 -0
- data/lib/rubino/interaction/input_queue.rb +117 -0
- data/lib/rubino/interaction/lifecycle.rb +299 -0
- data/lib/rubino/interaction/probe.rb +65 -0
- data/lib/rubino/interaction/state.rb +56 -0
- data/lib/rubino/jobs/cron_job_repository.rb +75 -0
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
- data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
- data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
- data/lib/rubino/jobs/queue.rb +184 -0
- data/lib/rubino/jobs/registry.rb +45 -0
- data/lib/rubino/jobs/runner.rb +79 -0
- data/lib/rubino/jobs/scheduler.rb +138 -0
- data/lib/rubino/jobs/webhook_delivery.rb +225 -0
- data/lib/rubino/jobs/worker.rb +59 -0
- data/lib/rubino/llm/adapter_factory.rb +47 -0
- data/lib/rubino/llm/adapter_response.rb +65 -0
- data/lib/rubino/llm/auxiliary_client.rb +61 -0
- data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
- data/lib/rubino/llm/content_builder.rb +55 -0
- data/lib/rubino/llm/credential_check.rb +93 -0
- data/lib/rubino/llm/error_classifier.rb +364 -0
- data/lib/rubino/llm/fake_provider.rb +292 -0
- data/lib/rubino/llm/inline_think_filter.rb +58 -0
- data/lib/rubino/llm/model_catalog.rb +29 -0
- data/lib/rubino/llm/provider_resolver.rb +48 -0
- data/lib/rubino/llm/reasoning_manager.rb +100 -0
- data/lib/rubino/llm/request.rb +56 -0
- data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
- data/lib/rubino/llm/scenario_loader.rb +68 -0
- data/lib/rubino/llm/scenario_selector.rb +80 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
- data/lib/rubino/llm/scenarios/analysis.yml +501 -0
- data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
- data/lib/rubino/llm/scenarios/failure.yml +65 -0
- data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
- data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
- data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
- data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
- data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
- data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
- data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
- data/lib/rubino/llm/thinking_support.rb +84 -0
- data/lib/rubino/llm/tool_bridge.rb +89 -0
- data/lib/rubino/logger.rb +99 -0
- data/lib/rubino/mcp/manager.rb +180 -0
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
- data/lib/rubino/mcp.rb +57 -0
- data/lib/rubino/memory/backend.rb +104 -0
- data/lib/rubino/memory/backends/default.rb +101 -0
- data/lib/rubino/memory/backends/sqlite.rb +653 -0
- data/lib/rubino/memory/backends.rb +53 -0
- data/lib/rubino/memory/deduplicator.rb +74 -0
- data/lib/rubino/memory/extractor.rb +85 -0
- data/lib/rubino/memory/flusher.rb +31 -0
- data/lib/rubino/memory/retriever.rb +50 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
- data/lib/rubino/memory/sqlite_graph.rb +154 -0
- data/lib/rubino/memory/store.rb +228 -0
- data/lib/rubino/memory/threat_scanner.rb +68 -0
- data/lib/rubino/metrics.rb +175 -0
- data/lib/rubino/modes.rb +93 -0
- data/lib/rubino/oauth/connection_repository.rb +95 -0
- data/lib/rubino/oauth/provider/github.rb +75 -0
- data/lib/rubino/oauth/provider/google.rb +59 -0
- data/lib/rubino/oauth/provider.rb +149 -0
- data/lib/rubino/oauth/registry.rb +86 -0
- data/lib/rubino/oauth/token_encryptor.rb +87 -0
- data/lib/rubino/plugins/registry.rb +75 -0
- data/lib/rubino/plugins.rb +86 -0
- data/lib/rubino/run/approval_gate.rb +243 -0
- data/lib/rubino/run/attachment_downloader.rb +166 -0
- data/lib/rubino/run/event_store.rb +74 -0
- data/lib/rubino/run/executor.rb +383 -0
- data/lib/rubino/run/gate_registry.rb +39 -0
- data/lib/rubino/run/recorder.rb +69 -0
- data/lib/rubino/run/repository.rb +118 -0
- data/lib/rubino/run/session_approval_cache.rb +118 -0
- data/lib/rubino/security/allowlist_persister.rb +55 -0
- data/lib/rubino/security/approval_policy.rb +227 -0
- data/lib/rubino/security/command_allowlist.rb +24 -0
- data/lib/rubino/security/dangerous_patterns.rb +118 -0
- data/lib/rubino/security/deny_persister.rb +73 -0
- data/lib/rubino/security/doom_loop_detector.rb +43 -0
- data/lib/rubino/security/hardline_guard.rb +105 -0
- data/lib/rubino/security/pattern_matcher.rb +62 -0
- data/lib/rubino/security/prefix_deriver.rb +124 -0
- data/lib/rubino/security/readonly_commands.rb +211 -0
- data/lib/rubino/session/exporter.rb +101 -0
- data/lib/rubino/session/message.rb +77 -0
- data/lib/rubino/session/repository.rb +295 -0
- data/lib/rubino/session/store.rb +198 -0
- data/lib/rubino/session/summary_store.rb +65 -0
- data/lib/rubino/skills/prompt_index.rb +85 -0
- data/lib/rubino/skills/registry.rb +208 -0
- data/lib/rubino/skills/skill.rb +176 -0
- data/lib/rubino/skills/skill_tool.rb +215 -0
- data/lib/rubino/skills/state_repository.rb +37 -0
- data/lib/rubino/skills/toggle.rb +26 -0
- data/lib/rubino/tools/answer_child_tool.rb +83 -0
- data/lib/rubino/tools/ask_parent_tool.rb +232 -0
- data/lib/rubino/tools/attach_file_tool.rb +120 -0
- data/lib/rubino/tools/background_tasks.rb +520 -0
- data/lib/rubino/tools/base.rb +222 -0
- data/lib/rubino/tools/custom_tool_loader.rb +119 -0
- data/lib/rubino/tools/edit_tool.rb +122 -0
- data/lib/rubino/tools/git_tool.rb +71 -0
- data/lib/rubino/tools/github_tool.rb +233 -0
- data/lib/rubino/tools/glob_tool.rb +69 -0
- data/lib/rubino/tools/grep_tool.rb +206 -0
- data/lib/rubino/tools/memory_tool.rb +184 -0
- data/lib/rubino/tools/multi_edit_tool.rb +110 -0
- data/lib/rubino/tools/patch_tool.rb +260 -0
- data/lib/rubino/tools/probe_tool.rb +175 -0
- data/lib/rubino/tools/question_tool.rb +128 -0
- data/lib/rubino/tools/read_attachment_tool.rb +180 -0
- data/lib/rubino/tools/read_tool.rb +212 -0
- data/lib/rubino/tools/read_tracker.rb +98 -0
- data/lib/rubino/tools/registry.rb +166 -0
- data/lib/rubino/tools/result.rb +113 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/session_search_tool.rb +103 -0
- data/lib/rubino/tools/shell_input_tool.rb +96 -0
- data/lib/rubino/tools/shell_kill_tool.rb +76 -0
- data/lib/rubino/tools/shell_output_tool.rb +72 -0
- data/lib/rubino/tools/shell_registry.rb +158 -0
- data/lib/rubino/tools/shell_tail_tool.rb +118 -0
- data/lib/rubino/tools/shell_tool.rb +330 -0
- data/lib/rubino/tools/steer_tool.rb +118 -0
- data/lib/rubino/tools/subagent_probe.rb +89 -0
- data/lib/rubino/tools/summarize_file_tool.rb +182 -0
- data/lib/rubino/tools/task_result_tool.rb +90 -0
- data/lib/rubino/tools/task_stop_tool.rb +80 -0
- data/lib/rubino/tools/task_tool.rb +622 -0
- data/lib/rubino/tools/test_tool.rb +454 -0
- data/lib/rubino/tools/todo_tool.rb +93 -0
- data/lib/rubino/tools/tool_call_repository.rb +33 -0
- data/lib/rubino/tools/vision_tool.rb +85 -0
- data/lib/rubino/tools/webfetch_tool.rb +153 -0
- data/lib/rubino/tools/websearch_tool.rb +179 -0
- data/lib/rubino/tools/write_tool.rb +61 -0
- data/lib/rubino/trust.rb +88 -0
- data/lib/rubino/ui/api.rb +296 -0
- data/lib/rubino/ui/base.rb +252 -0
- data/lib/rubino/ui/bottom_composer.rb +1599 -0
- data/lib/rubino/ui/cli.rb +1987 -0
- data/lib/rubino/ui/completion_menu.rb +321 -0
- data/lib/rubino/ui/completion_source.rb +284 -0
- data/lib/rubino/ui/escape_reader.rb +169 -0
- data/lib/rubino/ui/indented_io.rb +88 -0
- data/lib/rubino/ui/input_history.rb +108 -0
- data/lib/rubino/ui/live_region.rb +183 -0
- data/lib/rubino/ui/markdown_renderer.rb +506 -0
- data/lib/rubino/ui/notifier.rb +163 -0
- data/lib/rubino/ui/null.rb +195 -0
- data/lib/rubino/ui/paste_store.rb +176 -0
- data/lib/rubino/ui/printer_base.rb +79 -0
- data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
- data/lib/rubino/ui/queued_indicators.rb +66 -0
- data/lib/rubino/ui/status_bar.rb +100 -0
- data/lib/rubino/ui/stdout_proxy.rb +161 -0
- data/lib/rubino/ui/streaming_markdown.rb +186 -0
- data/lib/rubino/ui/subagent_cards.rb +134 -0
- data/lib/rubino/ui/subagent_view.rb +255 -0
- data/lib/rubino/ui.rb +21 -0
- data/lib/rubino/update_check.rb +187 -0
- data/lib/rubino/util/duration.rb +23 -0
- data/lib/rubino/util/hyperlink.rb +105 -0
- data/lib/rubino/util/output.rb +145 -0
- data/lib/rubino/util/secrets_mask.rb +83 -0
- data/lib/rubino/version.rb +5 -0
- data/lib/rubino/workspace.rb +85 -0
- data/lib/rubino-agent.rb +5 -0
- data/lib/rubino.rb +318 -0
- data/mise.toml +2 -0
- data/rubino-agent.gemspec +103 -0
- data/skills/ruby-expert/SKILL.md +67 -0
- data/skills/ruby-expert/references/concurrency.md +357 -0
- data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
- data/skills/ruby-expert/references/errors-and-types.md +460 -0
- data/skills/ruby-expert/references/gem-authoring.md +459 -0
- data/skills/ruby-expert/references/language-idioms.md +465 -0
- data/skills/ruby-expert/references/metaprogramming.md +339 -0
- data/skills/ruby-expert/references/oo-design.md +553 -0
- data/skills/ruby-expert/references/performance.md +383 -0
- data/skills/ruby-expert/references/rails.md +424 -0
- data/skills/ruby-expert/references/security.md +404 -0
- data/skills/ruby-expert/references/testing.md +473 -0
- data/skills/ruby-expert/references/tooling.md +466 -0
- metadata +856 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
Secure Ruby/Rails for an AI agent. Each vuln class shows the unsafe pattern next to the safe one. Targets Ruby 3.2–3.4, Rails 7.1–8.x. For app structure see references/rails.md; for the `send` injection note see references/metaprogramming.md; for regex performance (separate from ReDoS) see references/performance.md.
|
|
4
|
+
|
|
5
|
+
## SQL injection
|
|
6
|
+
|
|
7
|
+
Never interpolate user input into SQL fragments. Use parameterized queries — hash conditions, placeholders, or `sanitize_sql`.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# WRONG — string interpolation, trivially injectable
|
|
11
|
+
User.where("name = '#{params[:name]}'")
|
|
12
|
+
User.where("age > #{params[:age]}")
|
|
13
|
+
Order.order(params[:sort]) # order() is also injectable
|
|
14
|
+
|
|
15
|
+
# RIGHT — hash conditions (auto-parameterized + quoted)
|
|
16
|
+
User.where(name: params[:name])
|
|
17
|
+
User.where("age > ?", params[:age]) # positional placeholder
|
|
18
|
+
User.where("age > :age", age: params[:age]) # named placeholder
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Column names and SQL keywords can't be parameterized — allowlist them.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# WRONG — user controls the column/direction
|
|
25
|
+
Order.order("#{params[:col]} #{params[:dir]}")
|
|
26
|
+
|
|
27
|
+
# RIGHT — allowlist, never pass raw input as identifiers
|
|
28
|
+
SORTS = { "name" => "name", "date" => "created_at" }.freeze
|
|
29
|
+
col = SORTS.fetch(params[:col], "created_at")
|
|
30
|
+
dir = params[:dir] == "desc" ? "desc" : "asc"
|
|
31
|
+
Order.order(Arel.sql("#{col} #{dir}")) # Arel.sql asserts you vetted it
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`Arel.sql` silences Rails' "dangerous raw SQL" deprecation — only wrap values you have already proven safe. Other injectable methods that take raw SQL: `select`, `group`, `having`, `joins`, `pluck`, `lock`, `from`. The same hash/placeholder rules apply. `find_by_sql`/`execute` need `sanitize_sql_array`:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
sql = User.sanitize_sql_array(["SELECT * FROM users WHERE name = ?", params[:name]])
|
|
38
|
+
User.find_by_sql(sql)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Command injection
|
|
42
|
+
|
|
43
|
+
A string command goes through the shell, so metacharacters (`;`, `|`, `` ` ``, `$()`) inject. The array form bypasses the shell entirely.
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# WRONG — shell interpolation
|
|
47
|
+
system("convert #{params[:file]} out.png")
|
|
48
|
+
`git log #{ref}`
|
|
49
|
+
%x{ping #{host}}
|
|
50
|
+
exec("rm -rf #{dir}")
|
|
51
|
+
|
|
52
|
+
# RIGHT — array/multi-arg form: no shell, args passed literally
|
|
53
|
+
system("convert", params[:file], "out.png")
|
|
54
|
+
out = IO.popen(["git", "log", ref], &:read)
|
|
55
|
+
system("ping", "-c", "1", host)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`open`, `Open3.capture2/capture3`, `IO.popen`, `spawn`, `Process.spawn`, `Kernel#exec/system` all take the array form. Prefer `Open3` for capturing output safely:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
require "open3"
|
|
62
|
+
stdout, stderr, status = Open3.capture3("git", "log", "--oneline", ref)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Never pass untrusted input to `eval`, `instance_eval`, `class_eval`, `Kernel#open` with a `"|cmd"` string, or `send`/`public_send` with a user-supplied method name (see references/metaprogramming.md).
|
|
66
|
+
|
|
67
|
+
## Mass assignment
|
|
68
|
+
|
|
69
|
+
Use strong parameters; permit explicitly. Never `permit!` user input.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# WRONG — user can set admin: true, role:, etc.
|
|
73
|
+
User.create(params[:user])
|
|
74
|
+
User.update(params.require(:user).permit!)
|
|
75
|
+
|
|
76
|
+
# RIGHT — explicit allowlist; sensitive attrs never permitted
|
|
77
|
+
def user_params
|
|
78
|
+
params.require(:user).permit(:name, :email, :bio)
|
|
79
|
+
end
|
|
80
|
+
User.create(user_params)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Defense in depth for truly sensitive columns — block assignment at the model:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
class User < ApplicationRecord
|
|
87
|
+
attr_readonly :account_id # set once, never via update
|
|
88
|
+
# or expose a guarded setter and keep role= private
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Nested/array params must be declared:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
params.require(:order).permit(:note, line_items: [:sku, :qty], tag_ids: [])
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Unsafe deserialization
|
|
99
|
+
|
|
100
|
+
`Marshal.load`, `YAML.load` (pre-3.1 behavior), and `Oj` in object mode can instantiate arbitrary classes and trigger RCE. Never feed them untrusted bytes.
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# WRONG — RCE on attacker-controlled input
|
|
104
|
+
Marshal.load(request.body.read)
|
|
105
|
+
YAML.load(params[:config])
|
|
106
|
+
|
|
107
|
+
# RIGHT — safe_load with an explicit class allowlist
|
|
108
|
+
YAML.safe_load(params[:config]) # only basic types
|
|
109
|
+
YAML.safe_load(file, permitted_classes: [Date, Symbol], aliases: false)
|
|
110
|
+
|
|
111
|
+
# Prefer JSON for untrusted data
|
|
112
|
+
JSON.parse(request.body.read) # objects only, no code
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
On Ruby 3.1+/Psych 4, `YAML.load` is already an alias for `safe_load`; use `YAML.unsafe_load` only for files you fully control (and even then, prefer not to). Avoid `Marshal` for any cross-trust boundary — it has no safe mode.
|
|
116
|
+
|
|
117
|
+
## XSS in Rails
|
|
118
|
+
|
|
119
|
+
ERB auto-escapes by default. The danger is anything that opts out: `html_safe`, `raw`, `<%==`, and `sanitize` misuse.
|
|
120
|
+
|
|
121
|
+
```erb
|
|
122
|
+
<%# RIGHT — auto-escaped, safe %>
|
|
123
|
+
<%= @user.bio %>
|
|
124
|
+
|
|
125
|
+
<%# WRONG — renders raw HTML from user input %>
|
|
126
|
+
<%= raw @user.bio %>
|
|
127
|
+
<%= @user.bio.html_safe %>
|
|
128
|
+
<%== @user.bio %>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
When you must allow some HTML, use `sanitize` with an allowlist — never `html_safe`:
|
|
132
|
+
|
|
133
|
+
```erb
|
|
134
|
+
<%= sanitize @post.body, tags: %w[p br strong em a], attributes: %w[href] %>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Other XSS sinks:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# WRONG — user data into a script/JS context
|
|
141
|
+
"<script>var u = '#{params[:name]}';</script>".html_safe
|
|
142
|
+
# RIGHT
|
|
143
|
+
content_tag(:script, "var u = #{params[:name].to_json};".html_safe) # to_json escapes
|
|
144
|
+
|
|
145
|
+
link_to "site", params[:url] # WRONG: javascript: URLs execute
|
|
146
|
+
# RIGHT — validate scheme
|
|
147
|
+
url = params[:url].to_s
|
|
148
|
+
link_to "site", (url.start_with?("http://", "https://") ? url : "#")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Set a Content-Security-Policy (`config/initializers/content_security_policy.rb`) as a backstop. `html_safe` does not sanitize — it only marks a string as already-safe; calling it on user input is the bug.
|
|
152
|
+
|
|
153
|
+
## CSRF
|
|
154
|
+
|
|
155
|
+
Rails enables CSRF protection by default. Keep it on.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
class ApplicationController < ActionController::Base
|
|
159
|
+
protect_from_forgery with: :exception # default on new apps; don't remove
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Don't `skip_forgery_protection` or `skip_before_action :verify_authenticity_token` on state-changing actions. For JSON APIs authenticated by token/header (not cookies), use `ActionController::API` (no cookie session → CSRF N/A) rather than disabling the check on a cookie-session controller. Never globally disable it to "fix" a failing form.
|
|
164
|
+
|
|
165
|
+
## Open redirects
|
|
166
|
+
|
|
167
|
+
`redirect_to` with user input lets attackers bounce victims to phishing sites.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# WRONG — open redirect
|
|
171
|
+
redirect_to params[:return_to]
|
|
172
|
+
|
|
173
|
+
# RIGHT — Rails 7+ blocks off-host redirects by default; be explicit
|
|
174
|
+
redirect_to params[:return_to], allow_other_host: false # default
|
|
175
|
+
# Or allowlist paths only
|
|
176
|
+
safe = params[:return_to].to_s
|
|
177
|
+
redirect_to(safe.start_with?("/") && !safe.start_with?("//") ? safe : root_path)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Rails 7 made `allow_other_host: false` the default and raises on cross-host targets — do not set `allow_other_host: true` with user input.
|
|
181
|
+
|
|
182
|
+
## Authorization / IDOR
|
|
183
|
+
|
|
184
|
+
Insecure Direct Object Reference: a user passes an `id` for a record they don't own. Scope every lookup to the current user, and enforce a policy layer.
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
# WRONG — any user can read any invoice
|
|
188
|
+
@invoice = Invoice.find(params[:id])
|
|
189
|
+
|
|
190
|
+
# RIGHT — scope to ownership; 404s on someone else's record
|
|
191
|
+
@invoice = current_user.invoices.find(params[:id])
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Use a policy library and verify on every action:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
# Pundit
|
|
198
|
+
class InvoicePolicy < ApplicationPolicy
|
|
199
|
+
def show? = record.user_id == user.id
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def show
|
|
203
|
+
@invoice = Invoice.find(params[:id])
|
|
204
|
+
authorize @invoice # raises Pundit::NotAuthorizedError if denied
|
|
205
|
+
end
|
|
206
|
+
# enforce it globally
|
|
207
|
+
after_action :verify_authorized, except: :index
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
CanCanCan equivalent uses `authorize! :show, @invoice` against an `Ability`. Don't rely on hidden form fields or "the UI doesn't show it" — always check server-side. Don't trust `params[:user_id]` for the actor; derive identity from the session/token.
|
|
211
|
+
|
|
212
|
+
## Secrets management
|
|
213
|
+
|
|
214
|
+
Never commit secrets. Use Rails encrypted credentials or ENV; keep the master key out of git.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
EDITOR="code --wait" bin/rails credentials:edit # edits config/credentials.yml.enc
|
|
218
|
+
# config/master.key (and *.key) MUST be gitignored; ship the key via ENV in prod
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
Rails.application.credentials.dig(:stripe, :secret_key) # decrypted at runtime
|
|
223
|
+
ENV.fetch("DATABASE_URL") # fetch → fails loudly if unset
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# WRONG — secret hardcoded / committed
|
|
228
|
+
STRIPE_KEY = "sk_live_abc123"
|
|
229
|
+
ENV["SECRET"] || "fallback-secret" # fallback leaks into source
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Use the `dotenv-rails` gem for local dev ENV (gitignore `.env`, commit `.env.example` with blank values). Per-environment credentials: `config/credentials/production.yml.enc` + `config/credentials/production.key`. If a key leaks, rotate it — don't just remove it from the latest commit (it stays in history).
|
|
233
|
+
|
|
234
|
+
## Dependency security
|
|
235
|
+
|
|
236
|
+
Audit gems for known CVEs and keep them patched.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
gem install bundler-audit
|
|
240
|
+
bundle audit check --update # checks Gemfile.lock against ruby-advisory-db
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Run `bundle audit` in CI and fail the build on findings. Enable Dependabot (`.github/dependabot.yml`) or Renovate for automated PRs. Pin sources and avoid arbitrary git/path gems from untrusted origins:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
source "https://rubygems.org" # use HTTPS; don't add random sources
|
|
247
|
+
gem "rails", "~> 7.2.0" # pessimistic constraint, see tooling.md
|
|
248
|
+
gem "nokogiri", "~> 1.16"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Commit `Gemfile.lock` (apps) so CI/prod resolve identical versions. Review transitive deps; prefer well-maintained gems. For high-assurance setups, verify gem signatures (`gem cert` / `--trust-policy`), though most of the ecosystem is unsigned — rely primarily on the advisory DB + lockfile pinning. See references/tooling.md for Bundler details and references/gem-authoring.md for publishing.
|
|
252
|
+
|
|
253
|
+
## Static analysis — Brakeman
|
|
254
|
+
|
|
255
|
+
Brakeman is a Rails-specific security scanner; run it in CI.
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
gem install brakeman
|
|
259
|
+
brakeman --no-pager # scan
|
|
260
|
+
brakeman -w2 -z # warning level 2+, exit non-zero on findings (CI)
|
|
261
|
+
brakeman -I # interactively build the ignore file
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Triage findings into `config/brakeman.ignore` (with a note/justification per entry) for vetted false positives; never blanket-ignore. Pair with RuboCop's security cops. Brakeman catches SQLi, command injection, mass assignment, unsafe redirects, and `html_safe`/`raw` XSS sinks statically — but it is not a substitute for the safe patterns above.
|
|
265
|
+
|
|
266
|
+
## ReDoS and Timeout
|
|
267
|
+
|
|
268
|
+
Catastrophic backtracking lets a short input hang a regex (and your request thread). The risk is nested/overlapping quantifiers on user input.
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# WRONG — exponential backtracking on "aaaaaa!"
|
|
272
|
+
/^(a+)+$/ =~ user_input
|
|
273
|
+
/(\w+\s*)+$/ =~ user_input
|
|
274
|
+
|
|
275
|
+
# RIGHT — avoid nested quantifiers; anchor with \A \z (not ^ $, which match per-line)
|
|
276
|
+
/\A\w+\z/ =~ user_input
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Ruby 3.2+ ships a regexp timeout — set a global cap so no single match can hang:
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
Regexp.timeout = 1.0 # seconds, global (Ruby 3.2+)
|
|
283
|
+
/\A(a+)+\z/.match?(input, timeout: 0.5) # per-regexp override
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Validate input length before matching, and prefer non-regex parsing where possible. (Regex *performance/backtracking* internals: references/performance.md.) For other untrusted-duration work, `Timeout.timeout` exists but is unsafe — it can raise at arbitrary points and corrupt state; prefer the native regexp timeout or IO/socket-level timeouts instead.
|
|
287
|
+
|
|
288
|
+
## Random tokens and constant-time comparison
|
|
289
|
+
|
|
290
|
+
Use `SecureRandom` (CSPRNG) for anything secret — never `rand`, `Random`, or `SecureRandom.random_number` mod small ranges for tokens.
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# WRONG — predictable
|
|
294
|
+
token = rand(10**10).to_s
|
|
295
|
+
token = Time.now.to_i.to_s(36)
|
|
296
|
+
|
|
297
|
+
# RIGHT
|
|
298
|
+
SecureRandom.hex(32) # 64 hex chars
|
|
299
|
+
SecureRandom.urlsafe_base64(32)
|
|
300
|
+
SecureRandom.uuid # for ids, not secrets
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Compare secrets/tokens in constant time to avoid timing attacks — `==` short-circuits and leaks length/prefix info.
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
# WRONG — early-exit comparison leaks timing
|
|
307
|
+
provided == stored_token
|
|
308
|
+
|
|
309
|
+
# RIGHT — constant-time
|
|
310
|
+
ActiveSupport::SecurityUtils.secure_compare(provided, stored_token) # equal-length
|
|
311
|
+
ActiveSupport::SecurityUtils.fixed_length_secure_compare(a, b)
|
|
312
|
+
# plain Ruby:
|
|
313
|
+
OpenSSL.fixed_length_secure_compare(a, b) # raises unless same length; hash first if not
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Store password hashes with `has_secure_password` (bcrypt) — never plain or fast hashes (MD5/SHA) for passwords.
|
|
317
|
+
|
|
318
|
+
## TLS / certificate verification
|
|
319
|
+
|
|
320
|
+
Never disable certificate verification. It silently enables MITM.
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
# WRONG — accepts any cert
|
|
324
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
325
|
+
Net::HTTP.start(host, 443, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE)
|
|
326
|
+
OpenSSL::SSL::VERIFY_NONE # anywhere with untrusted peers
|
|
327
|
+
|
|
328
|
+
# RIGHT — verify (the default); use https URIs
|
|
329
|
+
uri = URI("https://api.example.com")
|
|
330
|
+
Net::HTTP.get(uri) # verifies by default
|
|
331
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
332
|
+
http.use_ssl = true # verify_mode defaults to VERIFY_PEER
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
If you hit cert errors, fix the trust store (`ssl_ca_cert`/`SSL_CERT_FILE`) — don't disable verification. Same rule for HTTP client gems (Faraday/HTTParty/Excon): never set `verify: false`/`ssl_verify: false` in production.
|
|
336
|
+
|
|
337
|
+
## SSRF
|
|
338
|
+
|
|
339
|
+
Server-Side Request Forgery: user-controlled URLs let attackers reach internal services (cloud metadata `169.254.169.254`, `localhost`, private ranges).
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
# WRONG — fetches whatever the user points at
|
|
343
|
+
Net::HTTP.get(URI(params[:url]))
|
|
344
|
+
|
|
345
|
+
# RIGHT — allowlist scheme + host, then resolve and block private IPs
|
|
346
|
+
require "resolv"; require "ipaddr"
|
|
347
|
+
|
|
348
|
+
def safe_fetch(raw)
|
|
349
|
+
uri = URI.parse(raw)
|
|
350
|
+
raise "scheme" unless %w[http https].include?(uri.scheme)
|
|
351
|
+
raise "host" unless ALLOWED_HOSTS.include?(uri.host) # allowlist is strongest
|
|
352
|
+
ip = IPAddr.new(Resolv.getaddress(uri.host))
|
|
353
|
+
raise "private" if ip.private? || ip.loopback? || ip.link_local?
|
|
354
|
+
Net::HTTP.get(uri)
|
|
355
|
+
end
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Prefer an explicit host allowlist over a denylist. Beware DNS-rebinding (resolve-then-connect on the same IP) and redirects (disable auto-follow or re-validate each hop). Block link-local (`169.254.0.0/16`) to protect cloud metadata endpoints.
|
|
359
|
+
|
|
360
|
+
## File uploads and path traversal
|
|
361
|
+
|
|
362
|
+
`../` in a filename can escape your directory. Never build paths from raw user input.
|
|
363
|
+
|
|
364
|
+
```ruby
|
|
365
|
+
# WRONG — path traversal: "../../etc/passwd"
|
|
366
|
+
File.read(File.join("uploads", params[:file]))
|
|
367
|
+
File.read("uploads/#{params[:name]}")
|
|
368
|
+
|
|
369
|
+
# RIGHT — basename strips directories; verify the result stays inside the root
|
|
370
|
+
name = File.basename(params[:file]) # drops any path components
|
|
371
|
+
path = File.expand_path(File.join(UPLOAD_DIR, name))
|
|
372
|
+
raise "traversal" unless path.start_with?(UPLOAD_DIR + File::SEPARATOR)
|
|
373
|
+
File.read(path)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
For uploads: allowlist extensions and validate content type, cap size, store outside the web root (or use Active Storage), and generate your own filename (`SecureRandom`) rather than trusting the client's.
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
ALLOWED_EXT = %w[.png .jpg .jpeg .pdf].freeze
|
|
380
|
+
ext = File.extname(uploaded.original_filename).downcase
|
|
381
|
+
raise "type" unless ALLOWED_EXT.include?(ext)
|
|
382
|
+
stored = "#{SecureRandom.uuid}#{ext}"
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Validate by sniffing real content (e.g. `Marcel`/magic bytes), not just the extension or the client-supplied MIME type. Never `send_file`/`render file:` with a user-controlled path without the basename+root check above.
|
|
386
|
+
|
|
387
|
+
## Quick checklist
|
|
388
|
+
|
|
389
|
+
- SQLi: `where(col: val)` / `where("x = ?", val)`; never interpolate; allowlist column/order identifiers; `Arel.sql` only on vetted strings.
|
|
390
|
+
- Command: array form `system("cmd", arg)` / `Open3.capture3`; never string commands, backticks, or `eval` with input.
|
|
391
|
+
- Mass assignment: strong params with explicit `permit`; never `permit!` user data; guard sensitive columns at the model.
|
|
392
|
+
- Deserialization: `YAML.safe_load` / `JSON.parse` on untrusted input; never `Marshal.load` or `YAML.unsafe_load` across trust boundaries.
|
|
393
|
+
- XSS: rely on auto-escaping; `sanitize` with allowlist, never `raw`/`html_safe` on user input; `to_json` for JS contexts; set CSP.
|
|
394
|
+
- CSRF: keep `protect_from_forgery`; use `ActionController::API` for token APIs instead of disabling it.
|
|
395
|
+
- Redirects: keep `allow_other_host: false`; allowlist or require leading `/`.
|
|
396
|
+
- AuthZ/IDOR: scope finds via `current_user.things.find`; enforce Pundit/CanCanCan on every action; verify server-side.
|
|
397
|
+
- Secrets: encrypted credentials or `ENV.fetch`; gitignore `*.key`/`.env`; rotate on leak; no hardcoded fallbacks.
|
|
398
|
+
- Deps: `bundle audit` in CI; Dependabot; commit `Gemfile.lock`; HTTPS source; pessimistic version pins.
|
|
399
|
+
- Scan: Brakeman `-w2 -z` in CI; triage into `brakeman.ignore` with justifications.
|
|
400
|
+
- ReDoS: avoid nested quantifiers; anchor with `\A\z`; set `Regexp.timeout`.
|
|
401
|
+
- Tokens: `SecureRandom`; compare with `SecurityUtils.secure_compare`; `has_secure_password` for passwords.
|
|
402
|
+
- TLS: never `VERIFY_NONE` / `verify: false`; fix the CA store instead.
|
|
403
|
+
- SSRF: allowlist scheme+host, resolve and block private/loopback/link-local IPs, re-validate redirects.
|
|
404
|
+
- Files: `File.basename` + `expand_path` + root prefix check; allowlist extension and sniff content; generate the stored filename.
|