Статьи

Проверьте параметры REST!

Я проводил исследования, связанные с моей продолжающейся серией «Студент проекта», и понял, что совершил одну из самых распространенных — и наиболее легко исправляемых — ошибок. Я не использовал все, что я знаю о веб-приложении, чтобы выдвинуть мой периметр безопасности наружу.

Я думаю конкретно о параметрах UUID. Я знаю, что каждый действительный внешне видимый идентификатор будет UUID. Я знаю форму UUID. Итак, почему я не проверяю, что мои параметры «uuid» являются потенциально допустимыми UUID, прежде чем идти дальше?

Это правда, что уровень базы данных не распознает неверное значение «uuid» — но это может не быть целью злоумышленника. Возможно, это часть атаки SQL-инъекцией. Возможно, это часть атаки XSS. Возможно, это часть атаки на мои журналы (например, путем включения действительно длинного значения, которое может вызвать переполнение буфера). Возможно, это часть того, о чем я никогда не слышал. Это не имеет значения — я всегда буду сильнее, удаляя известные неверные данные как можно быстрее.

Сервисный метод

Служебный метод, чтобы определить, является ли значение возможным UUID, использует простой шаблон регулярных выражений.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public final class StudentUtil {
    private static final Pattern UUID_PATTERN = Pattern
            .compile("^\\p{XDigit}{8}+-\\p{XDigit}{4}+-\\p{XDigit}{4}-\\p{XDigit}{4}+-\\p{XDigit}{12}$");
 
    /**
     * Private constructor to prevent instantiation.
     */
    private StudentUtil() {
 
    }
 
    public static boolean isPossibleUuid(String value) {
        return value != null && UUID_PATTERN.matcher(value).matches();
    }
}

Если мы хотим быть агрессивными, мы могли бы тщательно выбрать наши UUID, чтобы у них были дополнительные свойства, которые мы можем проверить. Например, соответствующий BigInteger всегда может иметь остаток от 3 мода 17. Маловероятно, что атака узнает об этом, и у нас будет предупреждение, когда кто-то исследует нашу систему. Еще более сложный подход будет использовать разные свойства для каждого класса UUID, например, UUID «курса» может быть 3 mod 17, а UUID «студента» — 5 mod 17.

Модульный тест

В наших тестах легко переборщить, но минимальный набор будет проверять не шестнадцатеричные цифры,
слишком много или слишком мало значений, пустая строка и нулевое значение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class StudentUtilTest {
 
    @Test
    public void testValidUuid() {
        assertTrue(StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c-6628952b41e1"));
    }
 
    @Test
    public void testInvalidUuid() {
        assertTrue(!StudentUtil.isPossibleUuid("63c7d68x-705c-4374-937c-6628952b41e1"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d68-8705c-4374-937c-6628952b41e1"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c4-374-937c-6628952b41e1"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-43749-37c-6628952b41e1"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c6-628952b41e1"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c-6628952b41e1a"));
        assertTrue(!StudentUtil.isPossibleUuid("63c7d688-705c-4374-937c-6628952b41e"));
        assertTrue(!StudentUtil.isPossibleUuid(""));
        assertTrue(!StudentUtil.isPossibleUuid(null));
    }
}

REST Server

Сервер REST должен проверить значение UUID для всех методов, для которых он требуется. Безопасно регистрировать параметр запроса после того, как мы убедились, что это правильно сформированный UUID, но все же необходимо соблюдать осторожность при регистрации неанизированных значений в запросе.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Path("/{courseId}")
    @GET
    @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })
    public Response getCourse(@PathParam("courseId") String id) {
 
        Response response = null;
        if (!StudentUtil.isPossibleUuid(id)) {
            response = Response.status(Status.BAD_REQUEST).build();
            LOG.info("attempt to use malformed UUID");
        } else {
            LOG.debug("CourseResource: getCourse(" + id + ")");
            try {
                Course course = finder.findCourseByUuid(id);
                response = Response.ok(scrubCourse(course)).build();
            } catch (ObjectNotFoundException e) {
                response = Response.status(Status.NOT_FOUND).build();
                LOG.debug("course not found: " + id);
            } catch (Exception e) {
                if (!(e instanceof UnitTestException)) {
                    LOG.info("unhandled exception", e);
                }
                response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
            }
        }
 
        return response;
    }

Очевидным улучшением является перемещение этой проверки (и списка исключений) в оболочку AOP для всех методов обслуживания. Это упростит код и позволит гарантировать, что проверки всегда выполняются. (В настоящее время я не использую его в Project Student, поскольку уровень сервера веб-сервиса в настоящее время не имеет зависимостей Spring.)

Вы можете привести веский аргумент opsec о том, что методы REST должны возвращать ответ NOT_FOUND вместо ответа BAD_REQUEST, чтобы уменьшить утечку информации.

Webapps

Детали различаются, но мы должны сделать то же самое с веб-приложениями, даже если они просто поверхностные интерфейсы для служб REST. Всякий раз, когда есть UUID, независимо от его источника, он должен быть проверен перед его использованием.

фильтры

Существует точка зрения, что безопасность должна осуществляться отдельно от приложения — что лучшая защита связана с развертыванием (через фильтры и AOP), а не внедряется в приложение. Никто не предлагает разработчикам приложений игнорировать соображения безопасности, просто проверки, которые я обсуждал выше, являются беспорядком, который отвлекает разработчика и ненадежен, поскольку разработчик может легко не заметить. Вместо этого они рекомендуют использовать АОП или фильтр.

Это просто написать фильтр, который выполняет ту же работу, что и код выше:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class RestParameterFilter implements Filter {
    private static final Logger LOG = Logger.getLogger(RestParameterFilter.class);
    private static final Set<String> validNouns = new HashSet<>();
 
    /**
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     */
    @Override
    public void init(FilterConfig cfg) throws ServletException {
 
        // learn valid nouns
        final String nouns = cfg.getInitParameter("valid-nouns");
        if (nouns != null) {
            for (String noun : nouns.split(",")) {
                validNouns.add(noun.trim());
            }
        }
    }
 
    /**
     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
     *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
     */
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException,
            ServletException {
 
        HttpServletRequest hreq = (HttpServletRequest) req;
        HttpServletResponse hresp = (HttpServletResponse) resp;
 
        // verify the noun + uuid
        if (!checkPathInfo(hreq, hresp)) {
            return;
        }
 
        // do additional tests, e.g., inspect payload
 
        chain.doFilter(req, resp);
    }
 
    /**
     * @see javax.servlet.Filter#destroy()
     */
    @Override
    public void destroy() {
    }
 
    /**
     * Check the pathInfo. We know that all paths should have the form
     * /{noun}/{uuid}/...
     *
     * @param req
     * @return
     */
    public boolean checkPathInfo(HttpServletRequest req, HttpServletResponse resp) {
        // this pattern only handles noun and UUID, no additional parameters.
        Pattern pattern = Pattern.compile("^/([\\p{Alpha}]+)(/?([\\p{XDigit}-]+)?)?");
        Matcher matcher = pattern.matcher(req.getPathInfo());
        matcher.find();
 
        // verify this is a valid noun.
        if ((matcher.groupCount() >= 1) && !validNouns.contains(matcher.group(1))) {
            // LOG.info("unrecognized noun");
            LOG.info("unrecognized noun: '" + matcher.group(1) + "'");
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return false;
        }
 
        // verify this is a valid verb.
        if ((matcher.groupCount() >= 4) && !StudentUtil.isPossibleUuid(matcher.group(4))) {
            LOG.info("invalid UUID");
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return false;
        }
 
        return true;
    }
}

Нет причин, по которым мы не можем также проверить полезную нагрузку. Например, мы можем проверить, правильно ли сформированы даты, номера телефонов и номера кредитных карт; или что имена включают только буквы (включая нелатинские символы, такие как ñ), пробелы и апострофы. (Вспомните «Anne-Marie Peña O’Brien».) Важно помнить, что эти проверки предназначены не для «достоверных» данных, а для устранения явно «недействительных» данных.

Мы должны добавить фильтр в наш файл web.xml.

web.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
<filter>
    <filter-name>REST parameter filter</filter-name>
    <filter-class>com.invariantproperties.sandbox.student.webservice.security.RestParameterFilter</filter-class>
     <init-param>
        <param-name>valid-nouns</param-name>
        <param-value>classroom,course,instructor,section,student,term,testRun</param-value>
    </init-param>
</filter>
 
<filter-mapping>
    <filter-name>REST parameter filter</filter-name>
    <servlet-name>REST dispatcher</servlet-name>
</filter-mapping>

ModSecurity

Легко написать фильтр для простых элементов, таких как телефонные номера и имена, но поля с открытым текстом — другое дело. Эти поля нуждаются в максимальной гибкости, в то же время мы хотим минимизировать риск XSS и других атак

Хорошим ресурсом в этом направлении является ModSecurity. Первоначально это был модуль Apache, но он был принят лабораториями Trustwave Spider. Он находится на веб-сервере, а не в веб-приложении, и проверяет данные, пересекающие его. Недавний порт (летом 2013 г.) позволяет настроить его с помощью фильтра сервлетов вместо внешнего обратного прокси-сервера. (Он использует JNI для инструментов содержащего сервер приложений.)

  • Для получения дополнительной информации см. ModSecurity for Java .

Справка: проверьте параметры REST! от нашего партнера JCG Bear Giles в блоге Invariant Properties .