View | Details | Raw Unified | Return to bug 51953
Collapse All | Expand All

(-)a/java/org/apache/catalina/filters/RemoteNetmaskFilter.java (+97 lines)
Line 0 Link Here
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 * 
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 * 
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
18
19
package org.apache.catalina.filters;
20
21
22
import org.apache.catalina.comet.CometEvent;
23
import org.apache.catalina.comet.CometFilterChain;
24
import org.apache.juli.logging.Log;
25
import org.apache.juli.logging.LogFactory;
26
27
import javax.servlet.FilterChain;
28
import javax.servlet.ServletException;
29
import javax.servlet.ServletRequest;
30
import javax.servlet.ServletResponse;
31
import java.io.IOException;
32
33
34
/**
35
 * Concrete implementation of <code>RequestNetmaskFilter</code> that filters
36
 * based on the string representation of the remote client's IP address.
37
 *
38
 */
39
40
public final class RemoteNetmaskFilter
41
    extends RequestFilter {
42
43
    // ----------------------------------------------------- Instance Variables
44
    private static final Log log = LogFactory.getLog(RemoteNetmaskFilter.class);
45
46
47
    // ------------------------------------------------------------- Properties
48
49
50
51
    // --------------------------------------------------------- Public Methods
52
53
54
    /**
55
     * Extract the desired request property, and pass it (along with the
56
     * specified request and response objects and associated filter chain) to
57
     * the protected <code>process()</code> method to perform the actual
58
     * filtering.
59
     *
60
     * @param request  The servlet request to be processed
61
     * @param response The servlet response to be created
62
     * @param chain    The filter chain for this request
63
     *
64
     * @exception java.io.IOException if an input/output error occurs
65
     * @exception javax.servlet.ServletException if a servlet error occurs
66
     */
67
    @Override
68
    public void doFilter(ServletRequest request, ServletResponse response,
69
            FilterChain chain) throws IOException, ServletException {
70
71
        process(request.getRemoteAddr(), request, response, chain);
72
73
    }
74
75
    /**
76
     * Extract the desired request property, and pass it (along with the comet
77
     * event and filter chain) to the protected <code>process()</code> method
78
     * to perform the actual filtering.
79
     *
80
     * @param event The comet event to be processed
81
     * @param chain The filter chain for this event
82
     *
83
     * @exception java.io.IOException if an input/output error occurs
84
     * @exception javax.servlet.ServletException if a servlet error occurs
85
     */
86
    @Override
87
    public void doFilterEvent(CometEvent event, CometFilterChain chain)
88
            throws IOException, ServletException {
89
        processCometEvent(event.getHttpServletRequest().getRemoteHost(),
90
                event, chain);        
91
    }
92
93
    @Override
94
    protected Log getLogger() {
95
        return log;
96
    }
97
}
(-)a/java/org/apache/catalina/filters/RequestNetmaskFilter.java (+269 lines)
Line 0 Link Here
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 * 
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 * 
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
18
19
package org.apache.catalina.filters;
20
21
22
import org.apache.catalina.comet.CometEvent;
23
import org.apache.catalina.comet.CometFilter;
24
import org.apache.catalina.comet.CometFilterChain;
25
import org.apache.catalina.util.NetMask;
26
27
import javax.servlet.FilterChain;
28
import javax.servlet.ServletException;
29
import javax.servlet.ServletRequest;
30
import javax.servlet.ServletResponse;
31
import javax.servlet.http.HttpServletResponse;
32
import java.io.IOException;
33
import java.net.InetAddress;
34
import java.net.UnknownHostException;
35
import java.util.ArrayList;
36
import java.util.List;
37
38
/**
39
 * Implementation of a Filter that performs filtering based on comparing the
40
 * appropriate request property (selected based on which subclass you choose
41
 * to configure into your Container's pipeline) against the regular expressions
42
 * configured for this Filter.
43
 * <p>
44
 * This filter is configured by setting the <code>allow</code> and/or
45
 * <code>deny</code> properties to a regular expressions (in the syntax
46
 * supported by {@link java.util.regex.Pattern}) to which the appropriate request property will
47
 * be compared.  Evaluation proceeds as follows:
48
 * <ul>
49
 * <li>The subclass extracts the request property to be filtered, and
50
 *     calls the common <code>process()</code> method.
51
 * <li>If there is a deny expression configured, the property will be compared
52
 *     to the expression. If a match is found, this request will be rejected
53
 *     with a "Forbidden" HTTP response.</li>
54
 * <li>If there is a allow expression configured, the property will be compared
55
 *     to the expression. If a match is found, this request will be allowed to
56
 *     pass through to the next filter in the current pipeline.</li>
57
 * <li>If a deny expression was specified but no allow expression, allow this
58
 *     request to pass through (because none of the deny expressions matched
59
 *     it).
60
 * <li>The request will be rejected with a "Forbidden" HTTP response.</li>
61
 * </ul>
62
 */
63
64
public abstract class RequestNetmaskFilter
65
    extends FilterBase implements CometFilter {
66
67
68
    // ----------------------------------------------------- Instance Variables
69
70
    /**
71
     * The regular expression used to test for allowed requests.
72
     */
73
    protected final List<NetMask> allow = new ArrayList<NetMask>();
74
75
    /**
76
     * The regular expression used to test for denied requests.
77
     */
78
    protected final List<NetMask> deny = new ArrayList<NetMask>();
79
80
    /**
81
     * mime type -- "text/plain"
82
     */
83
    private static final String PLAIN_TEXT_MIME_TYPE = "text/plain";
84
85
86
    // ------------------------------------------------------------- Properties
87
88
89
    /**
90
     * Return a string representation of the NetMask list in allow.
91
     */
92
    public String getAllow() {
93
        return allow.toString();
94
    }
95
96
97
    /**
98
     * Fill the allow list with the list of netmasks provided as an argument,
99
     * if any.
100
     *
101
     * @param input The list of netmasks, as a comma separated string
102
     */
103
    public void setAllow(final String input) {
104
        if (input == null || input.length() == 0)
105
            return;
106
107
        NetMask nm;
108
109
        for (final String s: input.split("\\s*,\\s*"))
110
            try {
111
                nm = new NetMask(s);
112
                allow.add(nm);
113
            } catch (IllegalArgumentException e) {
114
                // FIXME: log
115
            }
116
    }
117
118
119
    /**
120
     * Return a string representation of the NetMask list in deny.
121
     */
122
    public String getDeny() {
123
        if (deny == null) {
124
            return null;
125
        }
126
        return deny.toString();
127
    }
128
129
130
    /**
131
     * Fill the deny list with the list of netmasks provided as an argument,
132
     * if any.
133
     *
134
     * @param input The list of netmasks, as a comma separated string
135
     */
136
    public void setDeny(String input) {
137
        if (input == null || input.length() == 0)
138
            return;
139
140
        NetMask nm;
141
142
        for (final String s: input.split("\\s*,\\s*"))
143
            try {
144
                nm = new NetMask(s);
145
                deny.add(nm);
146
            } catch (IllegalArgumentException e) {
147
                // FIXME: log
148
            }
149
    }
150
151
152
    // --------------------------------------------------------- Public Methods
153
154
155
    /**
156
     * Extract the desired request property, and pass it (along with the
157
     * specified request and response objects) to the protected
158
     * <code>process()</code> method to perform the actual filtering.
159
     * This method must be implemented by a concrete subclass.
160
     *
161
     * @param request The servlet request to be processed
162
     * @param response The servlet response to be created
163
     * @param chain The filter chain
164
     *
165
     * @exception java.io.IOException if an input/output error occurs
166
     * @exception javax.servlet.ServletException if a servlet error occurs
167
     */
168
    @Override
169
    public abstract void doFilter(ServletRequest request,
170
            ServletResponse response, FilterChain chain) throws IOException,
171
            ServletException;
172
173
174
    // ------------------------------------------------------ Protected Methods
175
176
177
    /**
178
     * Perform the filtering that has been configured for this Filter, matching
179
     * against the specified request property.
180
     *
181
     * @param property The request property on which to filter
182
     * @param request The servlet request to be processed
183
     * @param response The servlet response to be processed
184
     * @param chain The filter chain
185
     *
186
     * @exception java.io.IOException if an input/output error occurs
187
     * @exception javax.servlet.ServletException if a servlet error occurs
188
     */
189
    protected void process(String property, ServletRequest request,
190
            ServletResponse response, FilterChain chain)
191
            throws IOException, ServletException {
192
193
        if (isAllowed(property)) {
194
            chain.doFilter(request, response);
195
            return;
196
        }
197
198
        if (!(response instanceof HttpServletResponse)) {
199
            sendErrorWhenNotHttp(response);
200
            return;
201
        }
202
203
        ((HttpServletResponse) response)
204
            .sendError(HttpServletResponse.SC_FORBIDDEN);
205
    }
206
207
    /**
208
     * Perform the filtering that has been configured for this Filter, matching
209
     * against the specified request property.
210
     *
211
     * @param property  The property to check against the allow/deny rules
212
     * @param event     The comet event to be filtered
213
     * @param chain     The comet filter chain
214
     * @exception java.io.IOException if an input/output error occurs
215
     * @exception javax.servlet.ServletException if a servlet error occurs
216
     */
217
    protected void processCometEvent(String property, CometEvent event,
218
            CometFilterChain chain) throws IOException, ServletException {
219
        HttpServletResponse response = event.getHttpServletResponse();
220
        
221
        if (isAllowed(property)) {
222
            chain.doFilterEvent(event);
223
            return;
224
        }
225
226
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
227
        event.close();
228
    }
229
230
    /**
231
     * Process the allow and deny rules for the provided property.
232
     * 
233
     * @param property  The property to test against the allow and deny lists
234
     * @return          <code>true</code> if this request should be allowed,
235
     *                  <code>false</code> otherwise
236
     */
237
    private boolean isAllowed(String property) {
238
        final InetAddress addr;
239
240
        try {
241
            addr = InetAddress.getByName(property);
242
        } catch (UnknownHostException e) {
243
            //Eh?
244
            return false;
245
        }
246
247
        for (final NetMask nm: deny)
248
            if (nm.matches(addr))
249
                return false;
250
251
        for (final NetMask nm: allow)
252
            if (nm.matches(addr))
253
                return true;
254
255
        // Allow if denies specified but not allows
256
        if (!deny.isEmpty() && allow.isEmpty())
257
            return true;
258
259
        // Deny this request
260
        return false;
261
    }
262
263
    private void sendErrorWhenNotHttp(ServletResponse response)
264
            throws IOException {
265
        response.setContentType(PLAIN_TEXT_MIME_TYPE);
266
        response.getWriter().write(sm.getString("http.403"));
267
        response.getWriter().flush();
268
    }
269
}
(-)a/java/org/apache/catalina/util/NetMask.java (+103 lines)
Line 0 Link Here
1
package org.apache.catalina.util;
2
3
import java.math.BigInteger;
4
import java.net.InetAddress;
5
import java.net.UnknownHostException;
6
7
/**
8
 * A class representing a netmask, which is at the core of this valve.
9
 *
10
 * <p>The constructor takes a {@link java.lang.String} representing a
11
 * CIDR netmask as an argument and extracts two informations from it: the
12
 * network address and the CIDR. It then turns the address into a {@link
13
 * java.math.BigInteger}, calculates the right shift and shifts that
14
 * BigInteger by it.</p>
15
 * <p>The process to verify whether an IP address falls within the mask
16
 * is to also convert it to a BigInteger, shifting it right and comparing
17
 * it to the stored BigInteger.
18
 * </p>
19
 */
20
21
public final class NetMask {
22
    /**
23
     * The argument to the constructor, used for .toString()
24
     */
25
    private final String expression;
26
27
    /**
28
     * The number of bits a matching candidate needs to be shifted right
29
     * in order to see if it matches
30
     */
31
    private final int shift;
32
33
    /**
34
     * The network address, already shifted right
35
     */
36
    private final BigInteger mask;
37
38
    /**
39
     * Constructor.
40
     *
41
     * @param expression the CIDR netmask
42
     * @throws IllegalArgumentException if the netmask is not correct
43
     * (invalid address specification, malformed CIDR prefix, etc)
44
     */
45
    public NetMask(final String expression) {
46
        final int idx = expression.indexOf("/");
47
        final int cidr, addrlen;
48
        final String addressPart;
49
        final InetAddress addr;
50
        final byte[] bytes;
51
52
        if (idx == -1) {
53
            cidr = -1;
54
            addressPart = expression;
55
        } else {
56
            final String substring = expression.substring(idx + 1);
57
            try {
58
                cidr = Integer.parseInt(substring);
59
                if (cidr < 0)
60
                    throw new NumberFormatException("CIDR is negative");
61
            } catch (NumberFormatException ignored) {
62
                throw new IllegalArgumentException("provided CIDR mask ("
63
                    + substring + ") is invalid");
64
            }
65
            addressPart = expression.substring(0, idx);
66
        }
67
68
        try {
69
            addr = InetAddress.getByName(addressPart);
70
        } catch (UnknownHostException e) {
71
            throw new IllegalArgumentException("provided address ("
72
                + addressPart + ") is invalid");
73
        }
74
75
        bytes = addr.getAddress();
76
        addrlen = bytes.length * 8;
77
        shift = cidr == -1 ? 0 : addrlen - cidr;
78
79
        if (shift < 0)
80
            throw new IllegalArgumentException("CIDR prefix (" + cidr
81
                + ") is greater than address length (" + addrlen + ")");
82
        mask = new BigInteger(bytes).shiftRight(shift);
83
        this.expression = expression;
84
    }
85
86
    /**
87
     * Test if a given address matches this netmask
88
     *
89
     * @param addr The {@link java.net.InetAddress} to test
90
     * @return true on match, false otherwise
91
     */
92
    public boolean matches (final InetAddress addr) {
93
        final BigInteger provided = new BigInteger(addr.getAddress())
94
            .shiftRight(shift);
95
96
        return mask.equals(provided);
97
    }
98
99
    @Override
100
    public String toString() {
101
        return expression;
102
    }
103
}
(-)a/java/org/apache/catalina/valves/RemoteNetmaskValve.java (+86 lines)
Line 0 Link Here
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 * 
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 * 
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
18
19
package org.apache.catalina.valves;
20
21
22
import org.apache.catalina.connector.Request;
23
import org.apache.catalina.connector.Response;
24
25
import javax.servlet.ServletException;
26
import java.io.IOException;
27
28
29
/**
30
 * Concrete implementation of <code>RequestFilterValve</code> that filters
31
 * based on the string representation of the remote client's IP address.
32
 *
33
 * @author Craig R. McClanahan
34
 * @version $Id$
35
 */
36
37
public final class RemoteNetmaskValve
38
    extends RequestNetmaskValve
39
{
40
41
42
    // ----------------------------------------------------- Instance Variables
43
44
45
    /**
46
     * The descriptive information related to this implementation.
47
     */
48
    private static final String info =
49
        "org.apache.catalina.valves.RemoteNetmaskValve/1.0";
50
51
52
    // ------------------------------------------------------------- Properties
53
54
55
    /**
56
     * Return descriptive information about this Valve implementation.
57
     */
58
    @Override
59
    public String getInfo() {
60
        return (info);
61
    }
62
63
64
    // --------------------------------------------------------- Public Methods
65
66
67
    /**
68
     * Extract the desired request property, and pass it (along with the
69
     * specified request and response objects) to the protected
70
     * <code>process()</code> method to perform the actual filtering.
71
     * This method must be implemented by a concrete subclass.
72
     *
73
     * @param request The servlet request to be processed
74
     * @param response The servlet response to be created
75
     *
76
     * @exception java.io.IOException if an input/output error occurs
77
     * @exception javax.servlet.ServletException if a servlet error occurs
78
     */
79
    @Override
80
    public void invoke(Request request, Response response)
81
        throws IOException, ServletException {
82
        process(request.getRequest().getRemoteAddr(), request, response);
83
    }
84
85
86
}
(-)a/java/org/apache/catalina/valves/RequestNetmaskValve.java (+220 lines)
Line 0 Link Here
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 * 
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 * 
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
18
19
package org.apache.catalina.valves;
20
21
22
import org.apache.catalina.connector.Request;
23
import org.apache.catalina.connector.Response;
24
import org.apache.catalina.util.NetMask;
25
26
import javax.servlet.ServletException;
27
import javax.servlet.http.HttpServletResponse;
28
import java.io.IOException;
29
import java.math.BigInteger;
30
import java.net.InetAddress;
31
import java.net.UnknownHostException;
32
import java.util.ArrayList;
33
import java.util.Arrays;
34
import java.util.Collections;
35
import java.util.List;
36
37
/**
38
 * Implementation of a Valve that performs IP filtering of a remote host
39
 * based on the remote's IP address. This valve replicates (some of) Apache's
40
 * "Order", "Allow from" and "Deny" directives with a few limitations:
41
 * <ul>
42
 *     <li>"Order" will always be "deny, allow";</li>
43
 *     <li>the netmasks in the "allow" and "deny" properties must be fully
44
 *     qualified CIDR netmasks, and the CIDR <b>must</b> be an integer
45
 *     (expressions like "10.", "13.0.0.0/255.255.192.0" or ".foo.bar" will
46
 *     not work);</li>
47
 *     <li>as a side effect of using @{link java.net.InetAddress},
48
 *     expressions like "my.host.com/27" <b>will</b> work,
49
 *     however their use is discouraged.</li>
50
 * </ul>
51
 * <p>Note that IPv6 is supported. Also note that invalid netmasks will be
52
 * ignored.</p>
53
 * <p>
54
 * This Valve may be attached to any Container, depending on the granularity
55
 * of the filtering you wish to perform.</p>
56
 *
57
 * @author Francis Galiegue
58
 */
59
60
public abstract class RequestNetmaskValve
61
    extends ValveBase {
62
63
    //------------------------------------------------------ Constructor
64
    public RequestNetmaskValve() {
65
        super(true);
66
    }
67
68
    // ----------------------------------------------------- Class Variables
69
70
71
    /**
72
     * The descriptive information related to this implementation.
73
     */
74
    private static final String info =
75
        "org.apache.catalina.valves.RequestNetmaskValve/1.0";
76
77
    // ----------------------------------------------------- Instance Variables
78
79
    /**
80
     * List of allowed netmasks, if any
81
     */
82
83
    protected final List<NetMask> allow = new ArrayList<NetMask>();
84
85
    /**
86
     * List of denied netmasks, if any
87
     */
88
89
    protected final List<NetMask> deny = new ArrayList<NetMask>();
90
91
    // ------------------------------------------------------------- Properties
92
93
94
    /**
95
     * Return the allowed netmask list as a string.
96
     */
97
    public String getAllow() {
98
        return allow.toString();
99
    }
100
101
102
    /**
103
     * Fills the allowed Netmask list.
104
     *
105
     * @param input The input expression
106
     */
107
    public void setAllow(final String input) {
108
        if (input == null || input.length() == 0)
109
            return;
110
111
        NetMask nm;
112
113
        for (final String s: input.split("\\s*,\\s*"))
114
            try {
115
                nm = new NetMask(s);
116
                this.allow.add(nm);
117
            } catch (IllegalArgumentException e) {
118
                // FIXME: log
119
            }
120
    }
121
122
123
    /**
124
     * Return the denied netmask list as a string.
125
     */
126
    public String getDeny() {
127
        return deny.toString();
128
    }
129
130
131
    /**
132
     * Set the regular expression used to test for denied requests for this
133
     * Valve, if any.
134
     *
135
     * @param input The new input expression
136
     */
137
    public void setDeny(final String input) {
138
       if (input == null || input.length() == 0)
139
           return;
140
141
        NetMask nm;
142
143
        for (final String s: input.split("\\s*,\\s*"))
144
            try {
145
                nm = new NetMask(s);
146
                this.deny.add(nm);
147
            } catch (IllegalArgumentException e) {
148
                // FIXME: log
149
            }
150
    }
151
152
153
    /**
154
     * Return descriptive information about this Valve implementation.
155
     */
156
    @Override
157
    public String getInfo() {
158
        return (info);
159
    }
160
161
162
    // --------------------------------------------------------- Public Methods
163
164
165
    /**
166
     * Extract the desired request property, and pass it (along with the
167
     * specified request and response objects) to the protected
168
     * <code>process()</code> method to perform the actual filtering.
169
     * This method must be implemented by a concrete subclass.
170
     *
171
     * @param request The servlet request to be processed
172
     * @param response The servlet response to be created
173
     *
174
     * @exception java.io.IOException if an input/output error occurs
175
     * @exception javax.servlet.ServletException if a servlet error occurs
176
     */
177
    @Override
178
    public abstract void invoke(Request request, Response response)
179
        throws IOException, ServletException;
180
181
182
    // ------------------------------------------------------ Protected Methods
183
184
185
    /**
186
     * Perform the filtering that has been configured for this Valve, matching
187
     * against the specified request property.
188
     *
189
     * @param property The request property on which to filter
190
     * @param request The servlet request to be processed
191
     * @param response The servlet response to be processed
192
     *
193
     * @exception java.io.IOException if an input/output error occurs
194
     * @exception javax.servlet.ServletException if a servlet error occurs
195
     */
196
    protected void process(String property, Request request, Response response)
197
        throws IOException, ServletException {
198
199
        final InetAddress addr = InetAddress.getByName(property);
200
201
        for (final NetMask nm: deny)
202
            if (nm.matches(addr)) {
203
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
204
                return;
205
            }
206
207
        if (allow.isEmpty()) {
208
            getNext().invoke(request, response);
209
            return;
210
        }
211
212
        for (final NetMask nm: allow)
213
            if (nm.matches(addr)) {
214
                getNext().invoke(request, response);
215
                return;
216
            }
217
218
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
219
    }
220
}

Return to bug 51953