Fail-open and fail-closed describe what a system does when a security control becomes unavailable. Failing open lets the operation continue without the check, preserving availability. Failing closed stops the operation, preserving the guarantee. For file uploads it decides whether an unscanned file is accepted or refused.
The choice, stated plainly
Every security control that sits in the path of a normal operation eventually becomes unavailable. The service is down, the network is unreachable, a certificate expired, a rate limit was hit, or a timeout fired because something was slow rather than broken.
At that moment your application has exactly two things it can do with the file in front of it, and there is no third that avoids the trade.
- Fail open. Accept the upload without scanning. Your service keeps working and your users are unaffected. You have accepted a file nobody checked, and you generally have no record marking it as different from any other
- Fail closed. Refuse the upload. Nothing unverified enters your system. A feature your users depend on is now broken, and if it sits in a revenue path or a customer-facing workflow, that is a real cost being paid in real time
Failing securely is a long-standing secure-design principle, set out in the OWASP secure product design guidance. The terminology is inherited from physical engineering, where a fail-safe door releases in a fire and a fail-secure door stays locked. Both are correct designs. Which one is right depends entirely on whether the greater danger is people being trapped inside or people getting in.
Why it usually gets decided by accident
The uncomfortable pattern is that most systems have a behaviour here that nobody chose. It emerged from a caught exception, a default timeout in an HTTP client, or a retry policy that gives up and returns success because that was the tidy thing to do at the time.
Failing open by accident is much more common than failing closed by accident, because it is what defensive coding tends to produce. A developer wraps a call in error handling so the request does not blow up, logs a warning, and carries on. That is good practice nearly everywhere else in an application, and here it quietly converts a security control into a suggestion.
The failure mode is silence. A fail-closed system announces itself immediately: uploads stop, users complain, somebody investigates. A fail-open system produces a line in a log that nobody reads, and everything appears to be fine. You find out what your policy was during the incident review.
This is worth testing rather than assuming. Point the integration at an address that does not answer, upload a file, and watch what happens. Whatever the system does is your actual policy, regardless of what the design document says.
The third option most applications should take
Framing this as a binary is what makes it feel unresolvable. In an application you control, you have an option a network appliance does not: you can accept the file without releasing it.
Accept, quarantine and mark unscanned. The upload succeeds from the user's point of view, so nothing is lost and the workflow continues. The file goes to a holding area rather than into normal circulation, flagged so that everything downstream knows it has not been verified. When scanning recovers, the backlog is processed and each file is either released or rejected, with the user told if theirs did not make it.
That keeps availability and the guarantee at the same time, and the cost is a state machine with one more state in it. What makes it work is that the file is not usable while it waits: it is not served to other users, not attached to outbound messages, not indexed, not previewed.
Whether this option is even available depends on your architecture. If you already scan asynchronously, you have the machinery, because a queue and a pending state are already there and an outage simply makes the queue longer. If you scan synchronously inside the request, quarantine means building that machinery, which is a real piece of work and usually the right one.
Deciding it deliberately
Four questions settle it more quickly than a general principle will.
- What happens to the file next? If it will be opened by a person, or served to other users, failing closed or quarantining is the right instinct. If it is data your own code parses in a sandbox, the calculation is different
- Can you re-check later? If you keep the original and can scan it before anything else touches it, accepting now costs much less. If it goes straight out to a third party, you have no second chance
- What does the user experience? A clear message saying checks are unavailable and to try shortly is a far better outcome than a generic error, and better again than silent acceptance
- Will you know? Whatever you choose, the degraded path needs to be loud. Alert on it, count it, and make the number visible, which is the observability the NCSC Cyber Assessment Framework expects of a control you are relying on. A control that stops working without telling anyone is the worst version of all three options
Test the degraded path the same way you test the happy one. Uploading an EICAR test file confirms that rejection works when the scanner is reachable; pointing the integration at an address that does not answer confirms what happens when it is not. Both belong in your release checks, and our guide to file upload security covers where they sit in the wider pipeline.
Whichever you pick, write it down where the next person will find it, and test it as part of your release process. This decision has a strong tendency to be reversed by accident during unrelated work.
The fewer outages you have to fail on, the easier this decision gets. The Red Eagle Tech CDR API offers synchronous and asynchronous endpoints with a documented error envelope, so your application can tell an outage from a rejection and act on the difference. Published per-document pricing, and you can be running in minutes.