Contaminated Squares

JavaScript, 105 97 bytes

Saved 8 bytes thanks to @Patrick Roberts!

l=a=>a.slice(1,-1)
p=a=>l(a).map(l)
c=a=>a.join``.replace(/[^0]/g,"")
s=a=>c(p(a))<c(a)?s(p(a)):a

Defines function s, which returns a 2D array of integers when provided a 2D array of integers as input.

How it Works

  • function l: given an array a, returns a copy without its first and last indexes.

  • function p: given a 2D array a, calls l to remove the first and last row, then for each remaining row calls l to remove the fist and last column. This performs the onion peeling.

  • function c: given a 2D array a, returns a string that only contains the 0s in the stringified form of a.

  • function s: given a 2D array a, calls c on the peeled form of the array given by p, and on the array itself. Compares these strings lexicographically to determine if the peeled form has less 0s than the original. If it does, then the original is contaminated, so call s recursively on the peeled form. Otherwise return the original.


Retina, 60 57 bytes

Byte count assumes ISO 8859-1 encoding. The trailing linefeed is significant.

+`(?<=(?=.*0|[^_]+(¶0|0¶|0.*$))^[^_]*)(^.+¶|¶.+$|.?\b.?)

Try it online!

Explanation

Due to the trailing linefeed, this finds all matches of the regex after the ` and removes them from the input. Due to the leading + this is done repeatedly until the output stops changing (which will be because the regex will stop matching).

As for the regex itself, it consists of two parts:

(?<=(?=.*0|[^_]+(¶0|0¶|0.*$))^[^_]*)

This part checks whether there's a 0 anywhere in the outer shell. It does this by moving the regex engine's "cursor" to the beginning of the string with a lookbehind (we use [^_] to match both digits and linefeeds):

(?<=...^[^_]*)

And then from that position we use a lookahead to find a 0 either in the first line, adjacent to a linefeed, or in the last line:

(?=.*0|[^_]+(¶0|0¶|0.*$))

Then the actual match will consist either of the first line (including its trailing linefeed), the last line (including its leading linefeed) or the first or last character of a line, where we abuse the word boundary \b as a beginning/end of line anchor:

(^.+¶|¶.+$|.?\b.?)

Jelly, 19 16 bytes

Fœ^F}P
ḊṖZµ⁺⁸ßç?

Try it online! or verify all test cases.

How it works

ḊṖZµ⁺⁸ßç?  Main link. Argument: M (2D list)

Ḋ          Dequeue; remove the first row.
 Ṗ         Pop; remove the last row.
  Z        Zip; transpose rows with columns.
   µ       Combine the chain to the left into a link.
    ⁺      Copy the link, executing it twice.
           The copy removes the first and last column and restores the orientation.
       ç?  If the helper link returns a non-zero integer:
     ⁸       Return M unmodified.
      ß      Else, recursively call the main link on the "peeled" M.


Fœ^F}P     Helper link. Arguments: P ("peeled" M), M (unmodified)

F          Flatten P.
   F}      Flatten M.
 œ^        Perform multiset symmetric difference, removing the elements of P from
           the elements of M, respecting multiplicities, leaving precisely the
           elements of the outer shell.
     P     Return the product of the remaining elements.