Ruby applications use YAML for configuration, fixtures, job payloads, and user-import files. Psych.safe_load is the right entry point for untrusted YAML because it limits which Ruby classes may be constructed. The limitation is easy to misread. Allowing a class means the parser may build that class. It does not validate the object graph, the size of aliases, or whether the application code that receives the object can safely process it.
Start with an allowlist that is actually small
Psych.safe_load raises Psych::DisallowedClass when the document contains a class that is not listed in permitted_classes. The default is intentionally restrictive. Pass only the types that the schema needs, such as Date when the document contains date fields. Do not pass a broad list copied from another service because one application needed symbols or a custom class.
That decision belongs beside the parser call and should have a test. A unit test should submit a YAML document containing an unapproved class and assert that parsing fails. A second test should submit the smallest valid document and confirm that the application still receives the expected typed values.
Aliases and size are separate controls
Psych has explicit handling for YAML aliases. Enabling aliases can be reasonable for trusted configuration, but a public upload endpoint should not enable them merely because a library example did. YAML can expand repeated structures, so the application also needs limits on input bytes, nesting depth, node count, and processing time.
The parser is not the whole ingestion service. Enforce a maximum request body before the YAML decoder sees it. Reject documents with unexpected top-level keys. Validate every extracted field against a schema and convert it into plain application objects. Do not pass a decoded hash directly into a template, a shell command, a database adapter, or a serializer that treats arbitrary keys as instructions.
Do not hide a fallback to unsafe loading
Applications occasionally catch a safe-load exception and retry with unsafe_load or YAML.load to preserve old behavior. That turns a controlled parser into an object-construction gadget. If a legacy file needs a broader type, migrate it with an offline conversion tool and keep the request path on the allowlist. Log the rejected class name and the source of the upload without logging the document body.
There is also an availability concern. A document can be syntactically valid and still expensive to process. Use a worker with memory and time limits for large files. Treat parser timeouts as rejected input, not as an invitation to raise the limit for every request.
Review the API contract
Define the YAML schema as data: field names, types, required values, maximum lengths, and nested object limits. Keep permitted_classes aligned with that schema. Revisit it when the application changes, and remove classes that no longer appear. Operators should be able to see which parser version, allowlist, and size limit handled each import.
Psych.safe_load removes a large class of Ruby object-injection risk. It does not make arbitrary YAML equal to a validated domain object. Keep the allowlist narrow, validate the shape, and let the request fail when the contract is not met.