Learn Ruby - Encoding, Input Validation & Safe Data Handling
Series/Learn Ruby/Episode 16
Episode 16 of 23

Learn Ruby - Encoding, Input Validation & Safe Data Handling

This episode covers safe data handling: UTF-8 encoding and string transcoding to prevent encoding errors, input validation and sanitization, strong params in Rails, password hashing with bcrypt, and the principle of least privilege.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Episode 15 closed the common holes with security tools. This episode 16 goes deeper: handling data correctly from the moment it arrives until it's stored. Two big themes are covered — string encoding and input validation — plus password hashing and the principle of least privilege.

Wrong encoding is the source of strange errors like ArgumentError: invalid byte sequence in UTF-8, which often appears in multilingual applications. Input validation prevents malformed data from entering the database. Password hashing ensures sensitive data stays safe even if the database leaks.

String Encoding in Ruby

Understanding String#encoding

Every string in Ruby carries encoding information. The String#encoding method returns its label, and String#valid_encoding? checks whether the contained bytes match that label:

RubyCheck a string's encoding
teks = "halo"
puts teks.encoding
puts teks.valid_encoding?

puts teks.encoding displays the default encoding of the source file. When reading external files or HTTP responses, don't assume UTF-8 — always check and adjust the encoding.

Transcoding and Error Prevention

To convert bytes from one encoding to another, use String#encode. If the bytes are invalid, encode throws an exception, so catch it with rescue or force character replacement:

RubyTranscoding with replacement
latin = teks.encode("ISO-8859-1")
puts latin.encoding
 
teks.encode("UTF-8", invalid: :replace, undef: :replace)

teks.encode("ISO-8859-1") produces a new string in Latin-1 encoding. The invalid: :replace option replaces unknown bytes with a replacement character instead of throwing an error. For web input, make sure the meta charset and Content-Type header consistently declare UTF-8.

Input Validation and Sanitization

Input Validation

Validation answers the question: is this data in the right shape? In ActiveRecord models, validation is declared declaratively:

RubyActiveRecord model validation
class Pengguna < ApplicationRecord
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :usia, numericality: { only_integer: true, greater_than: 0 }
end

validates :email, presence: true ensures an email exists, and the format is matched against the standard email pattern. numericality ensures age is a positive number. Model validation is the first line of defense; add client-side validation only for user experience, not security.

Strong Params in Rails

There's still a gap: mass assignment. An attacker can send hidden fields like admin: true. Rails closes it with strong params — a whitelist of fields allowed to be assigned:

RubyStrong params in Rails
def params_pengguna
  params.require(:pengguna).permit(:email, :nama, :usia)
end

params.require(:pengguna).permit(:email, :nama, :usia) only allows those three fields into the model. Other fields, including admin or role, are ignored. This principle also applies to query parameters in non-Rails applications: explicitly take every field you need.

Password Hashing with bcrypt

Storing raw passwords is the biggest violation. Store only the hash using bcrypt — an algorithm deliberately slow to slow down brute force. Rails already includes bcrypt through has_secure_password; for other projects add the gem:

Add bcrypt
gem install bcrypt
bundle add bcrypt

bundle add bcrypt adds the gem to your Gemfile and installs it. In code, hash the password at registration and verify at login:

RubyHash and verify passwords
require "bcrypt"
hash = BCrypt::Password.create("rahasia123")
puts BCrypt::Password.new(hash) == "rahasia123"

BCrypt::Password.create("rahasia123") produces a hash with an automatic random salt. Verification is done by re-comparing, not by decrypting — bcrypt is one-way. Never write logs containing passwords or tokens.

The Principle of Least Privilege

Least privilege means giving the minimum access needed to do the work. In databases, create an application account that doesn't have DROP or TRUNCATE rights. In code, don't give full capability to user input, and always verify authorization before sensitive operations:

RubyAuthorization before an action
def hapus_tugas(id)
  tugas = Tugas.find_by(id: id, pemilik_id: current_user.id)
  raise "Tidak berhak" unless tugas
  tugas.destroy
end

tugas.destroy only runs when the owner matches the currently logged-in user. Scoping patterns like this prevent IDOR (Insecure Direct Object Reference). Apply least privilege also to secrets, IAM, and service accounts.

Tip

Audit regularly: check database grant lists, unused environment keys, and deactivated users. Privileges that linger are unmonitored risk.

Conclusion

Key takeaways:

  • String#encoding and valid_encoding? are the first gateway to handling text.
  • String#encode with replacement options prevents crashes on invalid bytes.
  • Model validation and strong params filter data before it enters the database.
  • bcrypt stores passwords as one-way hashes with automatic salts.
  • Least privilege limits account, database, and secrets access to a minimum.
  • Always verify owner authorization before sensitive operations.

In episode 17 we move from security to performance: enabling YJIT with --yjit, measuring time with Benchmark, using ruby-prof to find hotspots, and understanding GC.stat and GC.config. The secure application we've built will now be made fast too.

Learn Ruby - Encoding, Input Validation & Safe Data Handling | Learn Ruby