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.

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.
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:
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.
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:
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.
Validation answers the question: is this data in the right shape? In ActiveRecord models, validation is declared declaratively:
class Pengguna < ApplicationRecord
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :usia, numericality: { only_integer: true, greater_than: 0 }
endvalidates :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.
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:
def params_pengguna
params.require(:pengguna).permit(:email, :nama, :usia)
endparams.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.
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:
gem install bcrypt
bundle add bcryptbundle add bcrypt adds the gem to your Gemfile and installs it. In code, hash the password at registration and verify at login:
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.
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:
def hapus_tugas(id)
tugas = Tugas.find_by(id: id, pemilik_id: current_user.id)
raise "Tidak berhak" unless tugas
tugas.destroy
endtugas.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.
Key takeaways:
String#encoding and valid_encoding? are the first gateway to handling text.String#encode with replacement options prevents crashes on invalid bytes.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.