{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"adversarial-inputs","title":"Inputs designed to hurt","url":"https://addaly.com/learn/how-software-changed/adversarial-inputs","position":26,"of":87,"minutes":9,"answer":"Keep a committed corpus of hostile inputs and fuzz anything that parses, remembering that a fuzzer finds crashes rather than wrong answers, so it layers with properties and differential runs rather than replacing them.","keyIdea":"Keep a committed corpus of hostile inputs and fuzz anything that parses, remembering that a fuzzer finds crashes rather than wrong answers, so it layers with properties and differential runs rather than replacing them."},"body":{"format":"markdown","text":"## The gap between the inputs you imagine and the inputs that arrive\n\nGenerated code handles the inputs that resemble its training. Real systems receive inputs that resemble the world. The gap between those two sets is where a large share of production incidents live, and it is cheap to explore on purpose.\n\nThere are two levels of effort here. The corpus, which takes twenty minutes and everyone should do. And fuzzing, which takes an afternoon and is worth it for anything that parses.\n\n## The corpus, which costs twenty minutes\n\nKeep a file of hostile inputs and run every parser, form handler and API endpoint against it. This one list has found more bugs in more codebases than any tool.\n\n**Emptiness and size.** Empty string. A single space. A single character. Ten megabytes of one character. A list of zero items. A list of a hundred thousand items. Deeply nested JSON, forty levels down: this crashes recursive parsers and it is a denial-of-service vector.\n\n**Unicode.** `José` in both normalisation forms, which compare unequal byte-for-byte and equal to a human. An emoji with a skin-tone modifier and a zero-width joiner, which is one grapheme and several code points and many bytes, so `len()` gives three different answers depending on what you asked. Right-to-left override characters, which make `file‮gnp.exe` display as something else entirely. The Turkish dotless ı, where lowercasing `I` gives different results by locale.\n\n**Numbers.** Zero. Negative zero. The maximum and minimum integers for your type. `0.1 + 0.2`. A number with fifteen significant digits arriving as a JSON float, which loses precision silently in JavaScript above 2^53. A quantity expressed as a string. A currency amount with three decimal places, because Kuwaiti dinars have them.\n\n**Time.** The end of February in a leap year. The day a clock goes forward, which in some zones means 02:30 does not exist. The day it goes back, when 01:30 happens twice. A timestamp in a timezone with a 45-minute offset: Nepal is UTC+05:45 and it is a real place with real users. A date before 1970. A date in the year 10,000, which breaks every fixed-width parser.\n\n**Names and text.** A name with an apostrophe, which is both a real name and an SQL injection test. A single-word name, common across much of Indonesia. A name longer than your column. A name in a script your font does not have. `NULL` as a surname, which is a real family name and breaks CSV pipelines constantly.\n\nWrite these into a fixtures file and parametrise your parser tests over it. Adding one line to that file after every incident is how it becomes valuable over time.\n\n## Fuzzing, for anything that parses\n\nA fuzzer generates inputs automatically, watches which code paths they reach, and mutates the ones that reach new territory. It is coverage-guided search, and it finds crashes that no human would think to write.\n\n```python\n# Python, with Atheris\nimport atheris, sys\nwith atheris.instrument_imports():\n    import my_parser\n\ndef one(data):\n    try:\n        my_parser.parse(data)\n    except my_parser.ParseError:\n        pass          # expected — anything else is a finding\n\natheris.Setup(sys.argv, one)\natheris.Fuzz()\n```\n\n```go\n// Go, built in since 1.18\nfunc FuzzParse(f *testing.F) {\n    f.Add([]byte(\"a=1\"))\n    f.Fuzz(func(t *testing.T, b []byte) { Parse(b) })\n}\n```\n\nAll free: libFuzzer and AFL++ for C and C++, `cargo fuzz` for Rust, Atheris for Python, Jazzer for Java, Go's built-in fuzzing. Run for ten minutes on your CSV parser, your config loader, your protocol decoder. Ten minutes is usually enough to find something.\n\nThe crashing input gets committed as a regression test. That is the whole workflow.\n\n## Where fuzzing does not reach\n\nA fuzzer finds crashes, hangs and assertion failures. It does not find wrong answers, because it has no idea what the right answer is. A parser that silently returns the wrong value for a valid input will fuzz clean for ever.\n\nSo the layers compose rather than substitute: fuzzing for \"does it fall over\", properties for \"is it consistent\", differential runs for \"does it match what we had\", and a human with a worked example for \"is it right\". Each covers a different failure and no one of them is enough. A team that has all four has a verification capacity that can absorb cheap generation. A team that has a green test suite has a number."},"check":{"question":"A team fuzzes their CSV parser for two hours with no crashes and concludes it is correct. A month later it silently drops rows whose final field is an unterminated quote. Why did fuzzing miss this?","options":[{"text":"Two hours is too short; coverage-guided fuzzing typically needs days of CPU time to reach deep parser states.","correct":false,"tempting":"Duration matters for reaching rare paths, but this input is trivially reachable: the fuzzer almost certainly generated it."},{"text":"CSV is a text format, and fuzzers work on binary inputs, so the generated mutations were rarely valid CSV.","correct":false,"tempting":"Coverage-guided fuzzers handle text formats routinely and their invalid inputs are the point."},{"text":"The parser did not crash on that input, and a fuzzer only detects crashes, hangs and assertion failures.","correct":true},{"text":"The seed corpus lacked a quoted-field example, so the fuzzer never explored the quoting branch of the parser.","correct":false,"tempting":"Seed quality genuinely shapes exploration, but coverage guidance would drive the fuzzer into that branch from almost any seed."}]},"prev":{"slug":"mutation-testing","title":"Proving the suite bites","url":"https://addaly.com/learn/how-software-changed/mutation-testing"},"next":{"slug":"flaky-tests","title":"A suite that lies intermittently","url":"https://addaly.com/learn/how-software-changed/flaky-tests"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Inputs designed to hurt', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/adversarial-inputs (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Inputs designed to hurt","courseTitle":"How Software Development Changed","module":{"index":3,"slug":"oracles-and-verification","title":"Building oracles that stay ahead"},"answer":null,"keyIdea":"Keep a committed corpus of hostile inputs and fuzz anything that parses, remembering that a fuzzer finds crashes rather than wrong answers, so it layers with properties and differential runs rather than replacing them.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"the-gap-between-the-inputs-you-imagine-and-the-inputs-that-a","runs":[{"text":"The gap between the inputs you imagine and the inputs that arrive"}]},{"t":"p","runs":[{"text":"Generated code handles the inputs that resemble its training. Real systems receive inputs that resemble the world. The gap between those two sets is where a large share of production incidents live, and it is cheap to explore on purpose."}]},{"t":"p","runs":[{"text":"There are two levels of effort here. The corpus, which takes twenty minutes and everyone should do. And fuzzing, which takes an afternoon and is worth it for anything that parses."}]},{"t":"h2","id":"the-corpus-which-costs-twenty-minutes","runs":[{"text":"The corpus, which costs twenty minutes"}]},{"t":"p","runs":[{"text":"Keep a file of hostile inputs and run every parser, form handler and API endpoint against it. This one list has found more bugs in more codebases than any tool."}]},{"t":"p","runs":[{"text":"Emptiness and size.","b":true},{"text":" Empty string. A single space. A single character. Ten megabytes of one character. A list of zero items. A list of a hundred thousand items. Deeply nested JSON, forty levels down: this crashes recursive parsers and it is a denial-of-service vector."}]},{"t":"p","runs":[{"text":"Unicode.","b":true},{"text":" "},{"text":"José","code":true},{"text":" in both normalisation forms, which compare unequal byte-for-byte and equal to a human. An emoji with a skin-tone modifier and a zero-width joiner, which is one grapheme and several code points and many bytes, so "},{"text":"len()","code":true},{"text":" gives three different answers depending on what you asked. Right-to-left override characters, which make "},{"text":"file‮gnp.exe","code":true},{"text":" display as something else entirely. The Turkish dotless ı, where lowercasing "},{"text":"I","code":true},{"text":" gives different results by locale."}]},{"t":"p","runs":[{"text":"Numbers.","b":true},{"text":" Zero. Negative zero. The maximum and minimum integers for your type. "},{"text":"0.1 + 0.2","code":true},{"text":". A number with fifteen significant digits arriving as a JSON float, which loses precision silently in JavaScript above 2^53. A quantity expressed as a string. A currency amount with three decimal places, because Kuwaiti dinars have them."}]},{"t":"p","runs":[{"text":"Time.","b":true},{"text":" The end of February in a leap year. The day a clock goes forward, which in some zones means 02:30 does not exist. The day it goes back, when 01:30 happens twice. A timestamp in a timezone with a 45-minute offset: Nepal is UTC+05:45 and it is a real place with real users. A date before 1970. A date in the year 10,000, which breaks every fixed-width parser."}]},{"t":"p","runs":[{"text":"Names and text.","b":true},{"text":" A name with an apostrophe, which is both a real name and an SQL injection test. A single-word name, common across much of Indonesia. A name longer than your column. A name in a script your font does not have. "},{"text":"NULL","code":true},{"text":" as a surname, which is a real family name and breaks CSV pipelines constantly."}]},{"t":"p","runs":[{"text":"Write these into a fixtures file and parametrise your parser tests over it. Adding one line to that file after every incident is how it becomes valuable over time."}]},{"t":"h2","id":"fuzzing-for-anything-that-parses","runs":[{"text":"Fuzzing, for anything that parses"}]},{"t":"p","runs":[{"text":"A fuzzer generates inputs automatically, watches which code paths they reach, and mutates the ones that reach new territory. It is coverage-guided search, and it finds crashes that no human would think to write."}]},{"t":"code","lang":"python","label":"Python","text":"# Python, with Atheris\nimport atheris, sys\nwith atheris.instrument_imports():\n    import my_parser\n\ndef one(data):\n    try:\n        my_parser.parse(data)\n    except my_parser.ParseError:\n        pass          # expected — anything else is a finding\n\natheris.Setup(sys.argv, one)\natheris.Fuzz()"},{"t":"code","lang":"go","label":"Go","text":"// Go, built in since 1.18\nfunc FuzzParse(f *testing.F) {\n    f.Add([]byte(\"a=1\"))\n    f.Fuzz(func(t *testing.T, b []byte) { Parse(b) })\n}"},{"t":"p","runs":[{"text":"All free: libFuzzer and AFL++ for C and C++, "},{"text":"cargo fuzz","code":true},{"text":" for Rust, Atheris for Python, Jazzer for Java, Go’s built-in fuzzing. Run for ten minutes on your CSV parser, your config loader, your protocol decoder. Ten minutes is usually enough to find something."}]},{"t":"p","runs":[{"text":"The crashing input gets committed as a regression test. That is the whole workflow."}]},{"t":"h2","id":"where-fuzzing-does-not-reach","runs":[{"text":"Where fuzzing does not reach"}]},{"t":"p","runs":[{"text":"A fuzzer finds crashes, hangs and assertion failures. It does not find wrong answers, because it has no idea what the right answer is. A parser that silently returns the wrong value for a valid input will fuzz clean for ever."}]},{"t":"p","runs":[{"text":"So the layers compose rather than substitute: fuzzing for “does it fall over”, properties for “is it consistent”, differential runs for “does it match what we had”, and a human with a worked example for “is it right”. Each covers a different failure and no one of them is enough. A team that has all four has a verification capacity that can absorb cheap generation. A team that has a green test suite has a number."}]}],"check":{"question":[{"text":"A team fuzzes their CSV parser for two hours with no crashes and concludes it is correct. A month later it silently drops rows whose final field is an unterminated quote. Why did fuzzing miss this?"}],"options":[{"text":[{"text":"Two hours is too short; coverage-guided fuzzing typically needs days of CPU time to reach deep parser states."}],"correct":false,"tempting":[{"text":"Duration matters for reaching rare paths, but this input is trivially reachable: the fuzzer almost certainly generated it."}]},{"text":[{"text":"CSV is a text format, and fuzzers work on binary inputs, so the generated mutations were rarely valid CSV."}],"correct":false,"tempting":[{"text":"Coverage-guided fuzzers handle text formats routinely and their invalid inputs are the point."}]},{"text":[{"text":"The parser did not crash on that input, and a fuzzer only detects crashes, hangs and assertion failures."}],"correct":true,"tempting":null},{"text":[{"text":"The seed corpus lacked a quoted-field example, so the fuzzer never explored the quoting branch of the parser."}],"correct":false,"tempting":[{"text":"Seed quality genuinely shapes exploration, but coverage guidance would drive the fuzzer into that branch from almost any seed."}]}],"order":[1,3,0,2],"why":null},"prev":{"slug":"mutation-testing","title":"Proving the suite bites","minutes":9},"next":{"slug":"flaky-tests","title":"A suite that lies intermittently","minutes":9}}}