This episode covers Ruby application security: common vulnerabilities like SQL injection, command injection, XSS, and unsafe deserialization, as well as the Brakeman and bundler-audit security tools and handling secrets through environment variables.

After successfully building a REST API in episode 14, the next question is: is your application secure? Security is often treated as the last layer, when in fact it should be thought about from the start. This episode 15 discusses security best practices in Ruby development.
We'll dissect the common vulnerabilities frequently found in Ruby applications — SQL injection, command injection, XSS, and unsafe deserialization — then get to know two important security tools: Brakeman for static analysis and bundler-audit for checking vulnerable gems. Finally, we'll cover handling secrets through environment variables.
SQL injection happens when user input is inserted directly into an SQL string. If you write queries with string interpolation, an attacker can inject malicious SQL:
user = params["username"]
User.where("name = '#{user}'")If user contains '; DROP TABLE users; --, the query above can damage a table. The safe way: use parameter placeholders. ActiveRecord secures automatically when you use parameterized syntax:
User.where("name = ?", params["username"])
User.where(name: params["username"])User.where("name = ?", ...) substitutes parameter values safely without direct interpolation. The principle: never paste raw input into an SQL string. User data may only enter as a bound parameter.
Command injection is similar to SQL injection, but its target is the system shell. Running system commands with interpolated user input is very dangerous:
system("ls #{params["folder"]}")If folder contains ; rm -rf /, the shell will execute the command after the semicolon. Use the array form so arguments aren't interpreted by the shell:
system("ls", params["folder"])system("ls", params["folder"]) runs the command without going through a shell, so shell characters are never evaluated. For complex needs, use libraries like Open3 and validate the input first.
XSS happens when user data is rendered as HTML without escaping. In Rails, ERB escapes output automatically with the <%= %> syntax, as long as you don't mark it as raw:
<%= user.comment %>
<%== user.comment %>The first line is safely escaped by Rails, while <%== %> skips escaping and triggers XSS if the content is user input. A simple rule: never display raw input to the browser without sanitization, and make sure Content-Security-Policy is active in the response headers.
Deserializing untrusted data is a serious hole. Marshal.load and YAML.load can execute code when loading malicious objects:
data = Marshal.load(payload)
safe = YAML.safe_load(payload)Marshal.load must never be used for user data. Use a safe format like JSON for data exchange, or YAML.safe_load, which rejects objects other than basic types. As a rule: only deserialize data you produced yourself.
Brakeman is a static security scanner specific to Rails. It reads source code and reports vulnerabilities without running the application:
gem install brakeman
brakeman --quietbrakeman --quiet produces a colorized report with a summary of warnings and the problematic files. Integrate Brakeman into CI with --exit-on-warn so the build fails when there are new warnings. The more often it runs, the faster vulnerabilities are found.
Vulnerabilities can also hide in third-party gems. bundler-audit checks Gemfile.lock against the advisories database:
gem install bundler-audit
bundle audit check --updatebundle audit check --update updates the advisory database then reports gems with vulnerable versions along with CVE references. Schedule this audit in CI on every dependency change, not just occasionally.
Never write API keys, database passwords, or tokens into source code. Store them all in environment variables, and read them with ENV:
db_password = ENV["DATABASE_PASSWORD"]
raise "DATABASE_PASSWORD belum diatur" if db_password.nil?ENV["DATABASE_PASSWORD"] retrieves a value from the environment. In development, use a gem like dotenv with a .env file that's in gitignore; in production, set secrets directly in the host environment or a vault. Rails itself provides bin/rails credentials for storing encrypted secrets.
Warning
Security is a process, not a status. The combination of Brakeman, bundler-audit, parameterized queries, and secrets in environment variables closes most common holes, but still do penetration tests and monitor advisories periodically.
Key takeaways:
system and exec.Marshal.load and YAML.unsafe_load must not be used for untrusted data.Gemfile.lock for vulnerable gems.In episode 16 we'll go deeper into safe data handling: UTF-8 encoding and string transcoding, input validation and sanitization, strong params in Rails, password hashing with bcrypt, and the principle of least privilege. Security from the data side completes the foundation we're building now.