Текст
                    75 РЕКОМЕНДАЦИЙ ПО НАПИСАНИЮ
НАДЕЖНЫХ И ЗАЩИЩЕННЫХ ПРОГРАММ
“Обязательно для чтения всем
разработчикам программ на Java”.
Мэри Энн Дэвидсон, начальник службы
информационной безопасности
компании Oracle
Руководство
для
ПРОГРАММИСТА
Фрэд Лонг, Дхрув Мохиндра, Роберт С. Сикорд,
Дин Ф. Сазерленд, Дэвид Свобода
Предисловие ДжеЙМСЗ А. ГоСЛИНГЭ, родоначальника языка программирования Java

Java Руководство ДЛЯ ПРОГРАММИСТА 75 РЕКОМЕНДАЦИЙ ПО НАПИСАНИЮ НАДЕЖНЫХ И ЗАЩИЩЕННЫХ ПРОГРАММ
Java' Coding Guidelines 75 Recommendations for Reliable and Secure Programms Red Long, Dhruv Mohindra, Robert C. Seacord, Dean F. Sutherland, David Svoboda Л Addison-Wesley Upper Saddle River, NJ • Boston • Indianapolis • San Francisco New York • Toronto • Montreal • London • Munich • Paris • Madrid Capetown • Sydney • Tokyo • Singapore • Mexico City
Java Руководство ДЛЯ ПРОГРАММИСТА 75 РЕКОМЕНДАЦИЙ ПО НАПИСАНИЮ НАДЕЖНЫХ И ЗАЩИЩЕННЫХ ПРОГРАММ Фрэд Лонг, Дхрув Мохиндра, Роберт С. Сикорд, Дин Ф. Сазерленд, Дэвид Свобода ВИЛЬЯМС Москва • Санкт-Петербург • Киев 2014
ББК 32.973.26-018.2.75 Р85 УДК 681.3.07 Издательский дом “Вильямс” Зав. редакцией С.Н. Тригуб Перевод с английского И.В. Берштейна По общим вопросам обращайтесь в Издательский дом “Вильямс” по адресу: info@williamspublishing.com, http://www.williamspublishing.com Лонг, Фрэд, Мохиндра, Дхрув, Сикорд, Роберт С., и др. Л76 Руководство для программиста на Java: 75 рекомендаций по написанию надежных и защищенных программ.: Пер. с англ. — М.: ООО “И.Д. Вильямс”, 2014. — 256 с.: ил. — Парал. тит. англ. ISBN 978-5-8459-1897-0 (рус.) ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответс- твующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фо- токопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Addison-Wesley Publishing Company, Inc. Authorized translation from the English language edition published by Addison-Wesley Publishing Company, Inc, Copyright © 2014 Pearson Education, Inc. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechan- ical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Russian language edition published by Williams Publishing House according to the Agreement with R&I Enterprises Interna- tional, Copyright © 2014. Научно-популярное издание Фрэд Лонг, Дхрув Мохиндра, Роберт С. Сикорд, Дин Ф. Сазерленд, Дэвид Свобода Руководство для программиста на Java: 75 рекомендаций по написанию надежных и защищенных программ Литературный редактор Верстка Художественный редактор Корректор И.А. Попова Л.В. Чернокозинская В.Г. Павлютин Л.А. Гордиенко Подписано в печать 21.03.2014. Формат 70x100/16 Гарнитура Times. Печать офсетная. Усл. печ. л. 20,64. Уч.-изд. л. 13,74. Тираж 1500 экз. Заказ № 3101 Первая Академическая типография “Наука” 199034, Санкт-Петербург, 9-я линия, 12/28 ООО “И. Д. Вильямс”, 127055, г. Москва, ул. Лесная, д. 43, стр. 1 ISBN 978-5-8459-1897-0 (рус.) ISBN 978-0-321-93315-7 (англ.) © Издательский дом “Вильямс”, 2014 © Pearson Education, Inc., 2014
Оглавление Предисловие ю Введение и Благодарности 17 Об авторах 19 Глава 1. Безопасность 23 Глава 2. Защитное программирование 91 Глава 3. Надежность 139 Глава 4. Понятность программ 171 Глава 5. Ложные представления программистов 209 Приложение A. Android 241 Приложение Б. Словарь специальных терминов 245 Приложение В. Библиография 249 Предметный указатель 254
Содержание Предисловие ю Введение и Безопасное программирование на Java 12 Предмет книги 13 Благодарности 17 Об авторах 19 От издательства 22 Глава 1. Безопасность 23 1. Ограничивайте срок действия уязвимых данных 24 2. Не храните уязвимые данные незашифрованными на стороне клиента 27 3. Снабжайте уязвимые изменяемые классы немодифицируемыми оболочками 30 4. Вызывайте уязвимые для безопасности методы с проверенными аргументами 32 5. Не допускайте выгрузку произвольных файлов 33 6. Кодируйте или экранируйте выводимые данные надлежащим образом 36 7. Предотвращайте внедрение кода 40 8. Предотвращайте внедрение операторов XPath 42 9. Предотвращайте внедрение операторов LDAP 46 10. Не пользуйтесь методом clone () для копирования небезопасных параметров метода 49 11. Не пользуйтесь методом Object, equals () для сравнения ключей шифрования 52 12. Не пользуйтесь небезопасными или слабыми алгоритмами шифрования 53 13. Храните пароли с помощью хеш-функции 54 14. Обеспечьте подходящее начальное случайное значение для класса SecureRandom 59 15. Не полагайтесь на методы, которые могут быть переопределены в ненадежном коде 60 16. Старайтесь не предоставлять излишние полномочия 66 17. Сводите к минимуму объем привилегированного кода 69 18. Не раскрывайте методы с нестрогими проверками ненадежного кода 70 19. Определяйте специальные полномочия доступа для мелкоструктурной защиты 78 20. Создавайте безопасную “песочницу” используя диспетчер защиты 81 21. Не допускайте злоупотреблений привилегиями методов обратного вызова в ненадежном коде 85 Глава 2. Защитное программирование 91 22. Минимизируйте область действия переменных 92 24. Минимизируйте доступность классов и их членов 96
Содержание 7 25. Документируйте потоковую безопасность и пользуйтесь аннотациями везде, где только можно 100 26. Всегда предоставляйте отклик на результирующее значение метода 106 27. Распознавайте файлы, используя несколько файловых атрибутов 109 28. Не присоединяйте значимость к порядковому значению, связанному с перечислением 115 29. Принимайте во внимание числовое продвижение типов 117 30. Активизируйте проверку типов в методах с переменным количеством аргументов во время компиляции 121 31. Не объявляйте открытыми и конечными константы, значения которых могут измениться в последующих выпусках программы 123 32. Избегайте циклических зависимостей пакетов 126 33. Отдавайте предпочтение определяемым пользователем исключениям над более общими типами исключений 128 34. Старайтесь изящно исправлять системные ошибки 130 35. Тщательно разрабатывайте интерфейсы, прежде чем их выпускать 132 36. Пишите код, удобный для “сборки мусора” 135 Глава 3. Надежность 139 37. Не затеняйте и не заслоняйте идентификаторы в подобластях действия 140 38. Не указывайте в одном объявлении больше одной переменной 142 39. Пользуйтесь описательными символическими константами для обозначения литеральных значений в логике программы 145 40. Правильно кодируйте отношения в определениях констант 148 41. Возвращайте из методов пустой массив или коллекцию вместо пустого значения 149 42. Пользуйтесь исключениями только в особых случаях 152 43. Пользуйтесь оператором try с ресурсами для безопасного обращения с закрываемыми ресурсами 154 44. Не пользуйтесь утверждениями для проверки отсутствия ошибок при выполнении 157 45. Пользуйтесь вторым и третьим однотипными операндами в условных выражениях 158 46. Не выполняйте сериализацию прямых описателей системных ресурсов 162 47. Отдавайте предпочтение итераторам над перечислениями 164 48. Не пользуйтесь прямыми буферами для хранения нечасто используемых объектов с коротким сроком действия 166 49. Удаляйте объекты с коротким сроком действия из контейнерных объектов с длительным сроком действия 167 Глава 4. Понятность программ 171 50. Будьте внимательны, применяя визуально дезориентирующие идентификаторы и литералы 171 51. Избегайте неоднозначной перегрузки методов с переменным количеством аргументов 175 52. Избегайте внутренних индикаторов ошибок 177 53. Не выполняйте операции присваивания в условных выражениях 179
8 Содержание 54. Пользуйтесь фигурными скобками в теле условного оператора if, а также циклов for или while 181 55. Не ставьте точку с запятой сразу после условного выражения с оператором if, for или while 183 56. Завершайте каждый набор операторов, связанных с меткой case, оператором break 184 57. Избегайте неумышленного зацикливания счетчиков циклов 186 58. Пользуйтесь круглыми скобками для обозначения операций предшествования 188 59. Не делайте никаких предположений о создании файлов 190 60. Преобразуйте целые значения в значения с плавающей точкой для выполнения операций с плавающей точкой 192 61. Непременно вызывайте метод super. clone () из метода clone () 195 62. Употребляйте комментарии единообразно и в удобном для чтения виде 197 63. Выявляйте и удаляйте излишний код и значения 198 64. Стремитесь к логической полноте 202 65. Избегайте неоднозначной или вносящей путаницу перегрузки 205 Глава 5. Ложные представления программистов 209 66. Не принимайте на веру, что объявление изменчивой ссылки гарантировало надежную публикацию членов объекта, доступного по этой ссылке 209 67. Не принимайте на веру, что методы sleep (), yield () или get St ate () предоставляли семантику синхронизации 215 68. Не принимайте на веру, что оператор вычисления остатка всегда возвращал неотрицательный результат для целочисленных операндов 219 69. Не путайте равенство абстрактных объектов с равенством ссылок 220 70. Ясно различайте поразрядные и логические операторы 223 71. Правильно интерпретируйте управляющие символы при загрузке строк 226 72. Не пользуйтесь перегружаемыми методами для динамического различения типов данных 229 73. Не путайте неизменяемость ссылки и доступного по ссылке объекта 231 74. Аккуратно пользуйтесь методами сериализации writeUnshared () и readUnshared() 235 75. Не пытайтесь оказывать помощь системе “сборки мусора”, устанавливая пустое значение в локальных переменных ссылочного типа 239 Приложение A. Android 241 Приложение Б. Словарь специальных терминов 245 Приложение В. Библиография 249 Предметный указатель 255
Моей покойной жене Энн — за ее любовь, помощь и поддержку в течение многих лет. Фрэд Лонг Моим родителям Дипаку и Эте Мохиндра, моей бабушке Шаши Мохиндра, а также нашему такому непоседливому, пятнистому далматинскому догу Гуглу. Дхрув Мохиндра Моей жене Алфи — за то, что эта книга была написана не зря, а также моим родителям Биллу и Лоуис — за то, что она вообще состоялась. Дэвид Свобода Моей жене Ронде и моим детям Челси и Джордану. Роберт С. Сикорд Либби, придающей всему смысл. Дин Ф. Сазерленд
Предисловие Рекомендации по программированию на Java, приведенные в этой книге, неоценимы пото- му, что они следуют нормам, установленным в выпущенном ранее справочном пособии по бе- зопасному программированию на Java (The CERT* Oracle9 Secure Coding Standard for Java"), Эту книгу следовало бы назвать “Руководство по надежному программированию на Java” Я уже давно обнаружил поразительную взаимосвязь между надежностью и безопасностью. Несмот- ря на применение действенных мер по обеспечению безопасности, включая шифрование, аутентификацию и прочее, для нарушения защиты чаще всего используются программные ошибки, обусловленные неудачным или обеспечивающим недостаточную защиту програм- мированием. Построение надежной системы во многом равнозначно построению безопасной системы. Поэтому усилия, затрачиваемые на обеспечение надежности, окупаются сторицей в отношении безопасности, и наоборот. В этой книге подчеркивается, что безопасность — это не средство, а внимательное отно- шение к каждой мелочи, которое должно стать неотъемлемой частью процесса разработки любого программного обеспечения. А организуется оно по определенному перечню рекомен- даций, в тонкостях которых и состоит вся суть этой книги. Например, рекомендация хранить пароли с помощью хеш-функции кажется, на первый взгляд, элементарной и вполне очевид- ной, и тем не менее регулярно появляются статьи о вновь обнаруженных серьезных наруше- ниях защиты, о которых разработчики программного обеспечения даже не подозревали. Сде- лать что-нибудь правильно не так-то просто, поскольку вся суть кроется в деталях. И в этой книге можно найти немало полезных рекомендаций, как разобраться во всех деталях. Джеймс А. Гослинг
Введение В этой книге даются конкретные рекомендации для программирующих на Java. Следуя этим рекомендациям, они смогут разрабатывать более надежные системы, устойчивые к на- рушению защиты. Эти рекомендации охватывают широкий спектр программных продуктов, разрабатываемых на Java для таких устройств, как ПК, игровые приставки, мобильные теле- фоны, планшетные компьютеры, бытовая техника и автомобильная электроника. Программирующие на любом языке должны придерживаться определенного ряда реко- мендаций, касающихся управления структурами их программ, а самое главное — того, что указано в определении языка программирования. И это в равной степени относится к Java. Для разработки надежных и безопасных программ на Java программистам требуется допол- нительная помощь, помимо того, что указано в спецификации языка программирования Java (JLS) [JLS 2013]. В состав Java входят языковые средства и прикладные программные интер- фейсы (API), которые можно легко употребить неправильно, и поэтому требуются рекомен- дации, помогающие обойти скрытые препятствия на пути к созданию надежных программ на Java. Для того чтобы программа была надежной, она должна работать во всех случаях и вопре- ки любым данным, которые могут быть введены. Любая нетривиальная программа не может избежать совершенно неожиданно возникающей ситуации в ходе ее выполнения или при вво- де данных, в результате чего возникают ошибки. Когда же возникают ошибки, очень важно ограничить их воздействие на программу, и для этого лучше всего локализовать ошибку и обработать ее как можно скорее. Стремясь предусмотреть неожиданные ситуации, которые могут возникнуть при вводе данных или в ходе выполнения программы, одни программис- ты могут выгодно воспользоваться опытом других, приняв на вооружение безопасный стиль программирования.
12 Введение Некоторые рекомендации по программированию носят стилистический характер, и все же они очень важны для обеспечения надежности и сопровождаемости кода. Для программиро- вания на Java в компании Oracle был установлен ряд правил оформления кода [Conventions 2009] с целью помочь разработчикам выработать устоявшийся стиль программирования, и с тех пор эти правила были повсеместно приняты программирующими на Java. Безопасное программирование на Java Предлагаемые читателям рекомендации по программированию на Java изложены автора- ми, написавшими ранее книгу The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012]. Описываемые в этой книге нормы оформления кода устанавливают ряд правил для безопас- ного программирования на Java. Эти правила нацелены на то, чтобы искоренить из практики привычку к небезопасному программированию, зачастую приводящему к появлению уязви- мостей, пригодных для незаконного использования. Стандарт безопасного программирова- ния на Java устанавливает нормативные требования для систем программного обеспечения. Такие системы могут быть оценены на соответствие данному стандарту, например, в Лабора- тории анализа исходного кода (Source Code Analysis Laboratory — SCALe) [Seacord 2012]. Тем не менее по-прежнему практикуются неправильные приемы программирования на Java, спо- собные привести к появлению ненадежных или небезопасных программ, несмотря на то, что они исключены из стандарта безопасного программирования на Java. Поэтому цель данной книги — рассмотреть эти неправильные приемы программирования на Java и предостеречь от их применения на практике. Несмотря на то что рассматриваемые здесь рекомендации не включены в материал книги The CERT9 Oracle9 Secure Coding Standard for Java™, это не преуменьшает их значение. Реко- мендации должны быть исключены из стандарта на программирование, если по ним нельзя составить нормативные требования. Имеется немало причин, по которым нельзя составить нормативное требование, и самая распространенная среди них состоит в том, что всякое правило зависит от намерения программиста. Подобные правила не могут быть соблюдены автоматически, если только не определено намерение программиста. В таком случае правило может потребовать согласования кода и определенного намерения. При составлении норма- тивного требования необходимо также предписать, что нарушение этого требования означает дефект в коде. Рекомендации были исключены из стандарта на программирование, но вошли в материал этой книги, в тех случаях, когда их целесообразно соблюдать, хотя их несоблюде- ние не всегда приводит к ошибке. Такое различие проводится для того, чтобы систему про- граммного обеспечения нельзя было считать не отвечающей стандарту в отсутствие конкрет- ного дефекта в ее коде. Следовательно, правила оформления кода должны быть очень строго определены. А рекомендации по программированию могут зачастую иметь намного более се- рьезное воздействие на безопасность и надежность, просто потому, что их можно определить менее строго, чем правила. Во многих из предлагаемых здесь рекомендаций делаются ссылки на правила, излагаемые в книге The CERT* Oracle9 Secure Coding Standard for Java™, описывающей одноименный стандарт, в такой форме: “IDS01-J. Нормализуйте символьные строки перед проверкой их достовернос- ти” где первые три буквы обозначают соответствующую главу данной книги. В частности, IDS обозначает главу 2 “Input Validation and Data Sanitization” (IDS — Проверка достоверности и санобработка данных).
Введение 13 Правила из стандарта безопасного программирования на Java доступны также на веб-сай- те, посвященном вопросам безопасного программирования (www. securecoding. cert. org), где они продолжают совершенствоваться. В стандарте, изложенном в книге The CERT* Oracle* Secure Coding Standard for Java™, дается определение правил для целей тестирования на соот- ветствие, а на упомянутом выше сайте можно найти дополнительные сведения или ценные выводы, отсутствующие в данной книге, но помогающие лучше интерпретировать смысл этих правил. Перекрестные ссылки на другие рекомендации делаются в тексте книги с указанием номера и названия соответствующей рекомендации. Предмет книги Эта книга посвящена вопросам программирования на платформе Java SE 7 и содержит рекомендации для написания безопасного кода с помощью прикладного программного ин- терфейса API для версии Java SE 7. В спецификации на язык программирования Java в версии Java SE 7 [JLS 2013] регламентируется поведение этого языка, и поэтому она послужила основ- ным источником для разработки рекомендаций, представленных в данной книге. В стандарты на такие традиционные языки программирования, как С и C++, включает- ся описание неопределенных, непредусмотренных и определенных в реализации видов по- ведения, которые могут привести к появлению уязвимостей, когда программист делает не- правильные предположения относительно переносимости этих видов поведения. Напротив, в спецификации на Java подобные виды поведения определены более полно, поскольку язык программирования Java разработан как независимый от конкретной платформы. Но и в этом случае некоторые виды поведения отдаются на откуп реализаторам виртуальной машины Java (JVM) или компилятора Java. Именно такие языковые особенности учитываются в пред- ставленных здесь рекомендациях, предлагающих реализаторам решение насущных вопросов и помогающих программистам правильно оценить и уяснить присущие языку ограничения, чтобы найти способы их преодоления. Для того чтобы писать надежные и безопасные программы, недостаточно уделить вни- мание вопросам одного только языка программирования. Иногда вопросы проектирования, возникающие при обращении к прикладным программным интерфейсам API языка Java, при- водят к тому, что эти интерфейсы становятся не рекомендуемые к дальнейшему применению. А порой интерфейсы API или документация на них интерпретируются программистами не- верно. На подобные неясности в интерфейсах API указывается в представленных здесь реко- мендациях, где обращается внимание на правильное их применение. Эти рекомендации до- полняются характерными образцами ошибочного проектирования или неверно выбранного стиля программирования. Ядро языка Java и его расширения в виде прикладных интерфейсов API, а также виртуаль- ная машина JVM предоставляют ряд средств для обеспечения безопасности, в том числе диспет- чер защиты и контроллер доступа, шифрование, автоматическое управление памятью, строгий контроль типов и проверку достоверности байт-кода. Эти средства обеспечивают достаточную безопасность для большинства приложений, но при условии, что они правильно используются, что очень важно. В представленных здесь рекомендациях указываются скрытые препятствия и предупреждаются опасности, связанные с архитектурой системы безопасности, а также де- лается акцент на правильную ее реализацию. Придерживаясь этих рекомендаций, можно убе- речься от многих программных ошибок, используемых для нарушения защиты с целью отказать в обслуживании, организовать утечку информации или намеренно превысить полномочия.
14 Введение Рассматриваемые библиотеки На рис. В.1 приведена концептуальная схема программных продуктов компании Oracle на платформе Java SE. Представленные здесь рекомендации по программированию направ- лены на разрешение вопросов, связанных, главным образом, с базовыми библиотеками, в том числе lang и util. Они позволяют избежать характерных программных ошибок, ко- торые уже исправлены или не имеют отрицательных последствий. А о функциональных программных ошибках упоминается лишь в тех случаях, если они происходят часто, пред- ставляют серьезную угрозу для безопасности и надежности или оказывают отрицательное воздействие на большинство технологических средств Java, опирающихся на базовую плат- форму. Эти рекомендации не ограничиваются только вопросами безопасности, характерны- ми для базового прикладного интерфейса API, но и касаются важных вопросов надежного и безопасного применения стандартных расширений этого прикладного интерфейса API (в пакете j avax). Язык Java Язык Java java javac javadoc jar javap JPDA Утилиты и служебное API JConsole JavaVisualVM JavaDB Безопасность IDL Развертывание Мониторинг Поискошибок Сценарии JVMTI Веб-службы Развертывание Java Web Start Аплеты/дополнения Java JavaFX Средства пользова- тельского интерфейса Библиотеки интеграции Другие базовые библиотеки Swing Java 2D AWT Доступность Перетаскивание Методы ввода . Службы печати Звук IDl JDBC JNDI RMI ЯМИЮР Сценарии Интернационализация Ввод-вывод JMX Beans Math Сетевые функции Безопасность Сериализация Механизм расширения Механизм замещения XMLJAXP Java SE API JAR Базовые библиотеки lang и util Ссылки на объекты Виртуальная машина Java Клиентская и серверная ВМ Java HotSpot Рис. В.1. Концептуальная схема программных продуктов компании Oracle на платформе Java SE. (Из документации на платформу Java SE компании Oracle, http://docs. oracle. com/javase/7/docs/. Copyright © 1995, 2010, компания Oracle и все ее филиалы. Все права защищены) Для демонстрации всего спектра средств, предлагаемых в Java для обеспечения безопас- ности, требуется исследовать взаимодействие кода с другими компонентами и каркасами приложений. В представленных здесь рекомендациях по программированию на Java иногда приводятся примеры, взятые из распространенных каркасов настольных и веб-приложений, включая Spring и Struts, а также технологии вроде Java Server Pages (JSP), чтобы показать уяз- вимость, которую нельзя рассматривать в отдельности. А библиотеки и прочие сторонние ре- шения предлагаются лишь в тех случаях, когда в стандартном прикладном интерфейсе API отсутствуют средства для полного или частичного устранения уязвимости.
Введение 15 Вопросы, не рассматриваемые в этой книге В представленных здесь рекомендациях по безопасному программированию на Java не рассматриваются следующие вопросы. Содержимое. Представленные здесь рекомендации по программированию распро- страняются на все платформы, но не охватывают вопросы, касающиеся только одной платформы на Java. В частности, рекомендации для платформы Android, Java Micro Edition (ME) или Java Enterprise Edition (ЕЕ), как правило, не годятся для Java Standard Edition (SE). Этими рекомендациями не охватываются также прикладные интерфейсы API в версии Java SE, поддерживающие воспроизведение звука и графики, управление доступом по учетным записям пользователей, организацию сеансов работы, аутенти- фикацию в пользовательском интерфейсе (и наборе инструментальных средств для него) или веб-интерфейсе. Тем не менее в этих рекомендациях обсуждаются сетевые системы на Java в отношении тех рисков, которые связаны с неправильной проверкой достоверности вводимых данных и внесением дефектов, а также предлагаются подхо- дящие методики для устранения подобных недостатков. В этих рекомендациях пред- полагается, что функциональные требования к программному продукту правильно выявляют и предотвращают уязвимости на более высоком уровне проектирования и разработки архитектуры. Стиль программирования. Вопросы стиля программирования носят довольно субъ- ективный характер, и поэтому выработать и дать подходящие рекомендации по этим вопросам не представляется возможным. Следовательно, в этой книге, как правило, соблюдение любого отдельно взятого стиля программирования не затрагивается. Вместо этого читателям рекомендуется выработать свои руководящие принципы отно- сительно стиля программирования и придерживаться их неукоснительно. Проще всего придерживаться выбранного стиля программирования с помощью инструментально- го средства для форматирования исходного кода. Такие средства предоставляются во многих интегрированных средах разработки (ИСР). Инструментальные средства. Многие из упоминаемых здесь рекомендаций непригод- ны для автоматического выявления или исправления ошибок. В некоторых случаях поставщики инструментальных средств могут реализовать проверку для выявления нарушений этих рекомендаций. Но Финансируемый государством научно-исследо- вательский центр (FFRDC) и Институт программотехники (SEI) не рекомендуют для этой цели конкретные инструментальные средства или их поставщиков. Противоречивые рекомендации. В целом из представленных здесь рекомендаций ис- ключены противоречивые и не нашедшие общего признания рекомендации. Кому адресована книга Эта книга адресована главным образом разработчикам программного обеспечения на Java. И хотя представленные в ней рекомендации касаются платформы Java SE 7, они могут приго- диться (хотя и не полностью) разработчикам, работающим на платформе Java ME или Java ЕЕ, а также тем, кто пользуется другими версиями Java. Несмотря на то что эти рекомендации предназначены для построения надежных и бе- зопасных систем, они окажутся полезными и для достижения других качеств, в том числе защищенности, безотказности, устойчивости, работоспособности и сопровождаемости. Эти рекомендации могут быть использованы следующими категориями специалистов.
16 Введение Разработчики инструментальных средств анализа, стремящиеся диагностировать про- граммы, небезопасные или несоответствующие нормам языка Java. Руководители групп разработчиков программного обеспечения, заказчики програм- много обеспечения и прочие специалисты как со стороны исполнителей, так и со сто- роны заказчиков программного обеспечения, которым требуется установить строгие нормы на безопасное программирование. Преподаватели, составляющие курсы программирования на Java. Содержание и организация книги Эта книга состоит из 75 рекомендаций, организованных по главам следующим образом. Глава 1, “Безопасность”. В этой главе представлены рекомендации по обеспечению бе- зопасности прикладных программ на Java. Глава 2, “Защитное программирование”. Эта глава содержит рекомендации по защит- ному программированию, позволяющие писать код, который сам себя защищает от всяких неожиданностей. Глава 3 “Надежность”. В этой главе даются рекомендации по повышению надежности и безопасности прикладных программ на Java. Глава 4, “Понятность программ”. В этой главе даются рекомендации, позволяющие сделать программы более понятными и удобочитаемыми. Глава 5, “Ложные представления программистов”. В этой главе рассматриваются ха- рактерные случаи, когда возникают недоразумения и ложные представления при про- граммировании на Java. В приложении А описывается применимость представленных в книге рекомендаций к разработке прикладных программ на Java для платформы Android. А в приложениях Б и В приводится словарь общеупотребительных терминов и библиографический перечень соот- ветственно. Представленные в книге рекомендации имеют согласованную структуру. В названии и вводном абзаце определяется сущность рекомендации. Далее обычно следует один или более пример кода, не соответствующий принятым нормам безопасного, надежного, понятного и корректного программирования на Java, а также решения, позволяющие привести код к этим нормам. Каждая рекомендация завершается разделом, посвященным ее применимости и биб- лиографическими ссылками на нее.
Благодарности Эта книга состоялась только благодаря усилиям широкого круга людей, помогавших ав- торам в работе над ней. Прежде всего нам хотелось бы поблагодарить тех, кто внес свой по- сильный вклад в составление рекомендаций, представленных в этой книге, в том числе Рона Бандеса (Bandes), Хозе Сандовала Чаверри (Jose Sandoval Chaverri), Райана Холла (Ryan Hall), Фея Хе (Fei Не), Райана Хофлера (Ryan Hofler), Сэма Каплана (Sam Kaplan), Майкла Кросса (Michael Kross), Кристофера Леонавициуса (Christopher Leonavicius), Боконга Лиу (Bocong Liu), Бастьяна Маркиза (Bastian Marquis), Аникета Мокаши (Aniket Mokashi), Джонатана Польсона (Jonathan Paulson), Майкла Розенмана (Michael Rosenman), Тамира Сена (Tamir Sen), Джона Трюлава (John Truelove), а также Метью Уитоффа (Matthew Wiethoff). Свой вклад в работу над этой книгой также внесли следующие люди, за что мы им искрен- не благодарны: Джеймс Альборн (James Ahlborn), Нилкамаль Гахарвар (Neelkamal Gaharwar), Анкур Гойял (Ankur Goyal), Тим Хэлоран (Tim Halloran), Суджай Джаин (Sujay Jain), Пранджал Джумде (Pranjal Jumde), Джастин Лу (Justin Loo), Ицхак Мандельбаум (Yitzhak Mandelbaum), Тодд Новацки (Todd Nowacki), Вишал Патель (Vishal Patel), Джастин Пинкар (Justin Pincar), Абхишек Рамани (Abhishek Ramani), Брендон Солсбери (Brendon Saulsbury), Кирк Сэйр (Kirk Sayre), Гленн Штроц (Glenn Stroz), Йозо Тода (Yozo Toda), а также Шишир Кумар Ядав (Shishir Kumar Yadav). Нам хотелось бы также поблагодарить Хироси Кумагаи (Hiroshi Kumagai) и группу японских специалистов из организации JPCERT за их работу над приложением к этой книге. Выражаем свою признательность рецензентам книги: Томасу Хоутену (Thomas Hawtin), Дэну Плакошу (Dan Plakosh), а также Стиву Школьнику (Steve Scholnick). Благодарим также руководителей организаций SEI и CERT, оказавших нам моральную поддержку, в том числе Арчи Эндрюса (Archie Andrews), Рича Петиа (Rich Pethia), Грега Шеннона (Greg Shannon), а также Билла Уилсона (Bill Wilson).
18 Благодарности Благодарим также нашего редактора Питера Гордона (Peter Gordon), редактора проекта Анну Попик (Anna Popick), литературного редактора Мелинду Ранкин (Melinda Rankin) и сле- дующих сотрудников издательства Addison-Wesley: Кима Бёдингхаймера (Kim Boedigheimer), Дженнифер Бортель (Jennifer Bortel), Джона Фуллера (John Fuller), Стефана Накиба (Stephane Nakib) и Джули Нахи л (Julie Nahil). Благодарим остальных сотрудников CERT за их моральную поддержку и посильную по- мощь, без чего эта книга не была бы завершена. И не в последнюю очередь нам хотелось бы поблагодарить своего собственного редактора Кэрол Дж. Лаллье (Carol J. Lallier), оказавшую нам немалую помощь в том, чтобы эта книга увидела свет.
Об авторах Фрэд Лонг является старшим преподавателем кафедры вычислитель- ной техники в Университете Аберистуита, Великобритания. Он читает лекции по формальным методам, языкам программирования Java, С++ и С, а также вопросам безопасности программирования. Он являет- ся председателем отделения Британского компьютерного общества в Уэльсе. Фрэд посещает Институт программотехники в США с 1992 года, тесно сотрудничая с его учеными. К числу последних его научных работ относится исследование уязвимостей в Java. Он является одним из авторов книги The CERT* Oracle9 Secure Coding Standard for Java", вы- шедшей в издательстве Addison-Wesley в 2012 г. Дхрув Мохиндра является ведущим специалистом в группе, подчи- ненной руководителю технического отдела компании Persistent Systems Limited, India, где он консультирует по вопросам информационной бе- зопасности в самых разных сферах деятельности, включая глобальную сеть, банковское дело и финансы, кооперацию, телесвязь, промышлен- ные предприятия, мобильную связь, науки о живой природе и здраво- охранение. Он регулярно консультирует высшее руководство и группы разработчиков из пятисот самых крупных компаний, средних и мелких предприятий, а также тех, кто начинает внедрять методики информа- ционной безопасности в жизненный цикл разработки программного обеспечения. Дхрув работал в отделе CERT Института программотехники (SEI) Карнеги-Меллона и продолжает сотрудничать с этими организациями для повышения культуры информацион- ной безопасности среди программирующих. Он получил степень магистра наук в области правил и управления информационной безопасностью в Университете Карнеги-Меллона. Он
20 Об авторах также имеет незаконченное высшее образование в области вычислительной техники, получив степень бакалавра в Университете г. Пун, Индия. Дхрув является одним из авторов книги The CERT* Oracle9 Secure Coding Standard for Java™, вышедшей в издательстве Addison-Wesley в 2012 г. Роберт С. Сикорд работает техническим руководителем по безопас- ному программированию в отделе CERT Института программотехни- ки (SEI) Карнеги-Меллона в Питтсбурге, шт. Пенсильвания. Он также является профессором Школы вычислительной техники и Институ- та информационных сетей при Университете Карнеги-Меллона. Ро- берт — автор книги The CERT* С Secure Coding Standard (издательство Addison-Wesley, 2009 г.), а также один из авторов книг Building Systems from Commercial Components (издательство Addison-Wesley, 2002 r.), Modernizing Legacy Systems (издательство Addison-Wesley, 2003 r.), The CERT* Oracle9 Secure Coding Standard for Java™ (издательство Addison- Wesley, 2012 г.) и Secure Coding in C and C++, Second Edition (издательс- тво Addison-Wesley, 2013 г.; в ИД “Вильямс” эта книга должна выйти в русском переводе в 2014 г.). Он опубликовал более шестидесяти статей по вопросам безопас- ности и разработки программного обеспечения из отдельных компонентов и веб-ориентиро- ванных систем, модернизации устаревших систем, организации информационных хранилищ компонентов, а также проектирования пользовательских интерфейсов. Роберт обучает безопасному программированию на С и C++ в частном секторе промыш- ленности, академической и государственной сферах с 2005 года. Профессионально програм- мировать он начал еще в 1982 году в корпорации IBM, разрабатывая программное обеспече- ние для передачи данных и операционных систем, процессоров и других приложений. Роберт работал также в компании X Consortium, Inc., где разрабатывал и сопровождал программы для среды рабочего стола Common Desktop Environment и операционной системы X Window System. Он представляет Университет Карнеги-Меллона в международной рабочей группе ISO/IEC JTC1/SC22/WG14 по стандартизации языка программирования С. Дин Сазерленд работает старшим инженером по безопасности про- граммного обеспечения в организации CERT (Computer Security Incident Response Team — Группа быстрого реагирования на инциден- ты, связанные с нарушением компьютерной безопасности). В 2008 году он получил степень доктора философских наук в области проектиро- вания программного обеспечения в Университете Карнеги-Меллона. Прежде чем вернуться в академическую сферу, он проработал четыр- надцать лет инженером по безопасности программного обеспечения в компании Tartan, Inc., из которых последние шесть лет руководил груп- пой, занимавшейся разработкой внутренних программ компилятора. В этой компании он руководил опытно-конструкторскими работами и проектами, занимался внедрением новых технологий в процесс разработки и развертывание программного обеспечения. Дин является одним из авторов книги The CERT* Oracle9 Secure Coding Standard for Java™, вышедшей в издательстве Addison-Wesley в 2012 г.
Об авторах 21 Дэвид Свобода работает инженером по безопасности программного обеспечения в отделе CERT Института программотехники (SEI) Кар- неги-Меллона и является одним из авторов книги The CERT* Oracle9 Secure Coding Standard for Java™, вышедшей в издательстве Addison- Wesley в 2012 г. Он также сопровождает веб-сайты CERT, посвященные безопасному программированию на Java, С, C++ и Perl. С 1991 года Дэвид участвовал в качестве ведущего разработчика программного обеспечения в различных проектах Института программотехники, включая иерархическое моделирование микросхем, моделирование социальных организаций и автоматизированный машинный перевод. Его программа машинного перевода KANTOO АМТ, разработанная еще в 1996 году, эксплу- атируется до сих пор в корпорации Caterpillar. У него имеется более чем 13-летний опыт разработки программ на Java, начиная с версии Java 2, а к числу выполненных им проектов относятся сервлеты Tomcat и модули, подключаемые к ИСР Eclipse. Он обучал безопасному программированию на С и C++ различные группы по всему миру из военной, государствен- ной и банковской сфер. Кроме того, Дэвид — активный участник рабочих групп ISO/IEC JTC1/SC22/WG14 и ISO/IEC JTC1/SC22/WG21 по стандартизации языков программирования С и C++ соответственно.
22 Об авторах От издательства Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мне- ние и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замеча- ния, которые вам хотелось бы высказать в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо либо просто посетить наш веб-сайт и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию последующих книг. Наши электронные адреса: E-mail: inf o@williamspublishing. com WWW: http://www.williamspublishing.com Наши почтовые адреса: в России: 127055, г. Москва, ул. Лесная, д.43, стр. 1 в Украине: 03150, Киев, а/я 152
Глава Безопасность Язык программирования и исполняющая система Java были разработаны с учетом безо- пасности. В частности, манипулирование указателями сделано неявным и скрытым от про- граммиста, а любая попытка сослаться на пустой указатель приводит к генерированию ис- ключения. Аналогично исключение возникает и при попытке доступа к данным за пределами массива или символьной строки. Язык Java является строго типизированным, и поэтому все неявные преобразования типов вполне определены и не зависят от конкретной платформы, как, впрочем, и арифметические типы данных и их преобразования. В виртуальной машине Java (JVM) имеется встроенный верификатор байт-кода, обеспечивающий соответствие вы- полняемого байт-кода спецификации языка программирования Java (JLS), а именно Java SE 7 Edition. Благодаря этому все проверки определены в языке на своих местах и не могут быть обойдены. Механизм загрузки классов в Java распознает классы при их загрузке в виртуальную машину JVM и способен отличать доверенные системные классы от других классов, кото- рым, возможно, не стоит доверять. Классам из внешних источников могут быть даны пра- ва на цифровую подпись. Эти цифровые подписи также могут быть проверены загрузчиком классов, внося свою лепту в распознание класса. В Java предоставляется также расширяемый механизм мелкоструктурной защиты, позволяющий программисту управлять доступом к та- ким ресурсам, как системная информация, файлы, сокеты и прочие уязвимые с точки зрения безопасности ресурсы, которыми программист желает воспользоваться. Механизм защиты может потребовать наличия диспетчера защиты для соблюдения правил безопасности. Дис- петчер защиты и его правила безопасности обычно определяются с помощью аргументов ко- мандной строки, но они могут быть установлены программным способом, при условии, что такое действие еще не запрещено существующими правилами безопасности. Права на доступ к ресурсам могут быть распространены на несистемные классы Java, исходя из результатов их распознавания, предоставляемых механизмом загрузки классов.
24 Глава 1 Безопасность Приложения масштаба предприятия на Java подвержены атакам с целью нарушить защиту, поскольку они принимают небезопасные вводимые данные и взаимодействуют со сложными системами. Атаки с умышленным внедрением кода (например, межсайтовое выполнение сце- нариев [XSS], внедрение операторов LDAP и XPath) становятся вполне возможными, когда в приложении используются компоненты, подверженные подобным атакам. В качестве эф- фективной меры борьбы с подобными нарушениями безопасности служит внедрение белых списков, кодирование или экранирование выводимых данных перед их обработкой для вос- произведения. В этой главе представлены рекомендации, касающиеся обеспечения безопас- ности приложений на Java. В ней ясно сформулированы рекомендации, имеющие отношение к следующим особенностям обеспечения безопасности. 1. Обращение с уязвимыми данными. 2. Предотвращение типичных атак с умышленным внедрением. 3. Применение языковых средств, неправильное обращение с которыми может нарушить безопасность. 4. Учет особенностей работы механизма мелкоструктурной защиты в Java. 1. Ограничивайте срок действия уязвимых данных Конфиденциальность уязвимых данных, находящихся в оперативной памяти, может быть нарушена. Нарушитель, способный выполнить код в той же системе, что и приложение, мо- жет получить доступ к таким данным, если в приложении: для хранения уязвимых данных используются объекты, которые не очищаются или не собираются в “мусор” после их применения; имеются страницы памяти, которые могут быть выгружены на диск, если того пот- ребует операционная система (например, для выполнения задач управления памятью или поддержки спящего режима); и уязвимые данные находятся в буфере (например, типа Buf feredReader), где сохраня- ются копии данных из кеша операционной системы или оперативной памяти; управляющая логика опирается на рефлексию, позволяющую принять меры противо- действия, чтобы преодолеть ограничение на срок действия уязвимых переменных; уязвимые данные обнаруживаются в отладочных сообщениях, файлах журналов ре- гистрации, переменных окружения, потоках исполнения или разгрузках оперативной памяти. Утечка уязвимых данных может оказаться более вероятной, если оперативная память, в которой они хранятся, не очищается после их применения. Для того чтобы снизить риск рас- крытия уязвимых данных в прикладных программах, следует свести к минимуму срок дейс- твия этих данных. Для полноценной, надежной защиты данных в оперативной памяти требуется поддержка со стороны базовой операционной системы и виртуальной машины Java. Так, если встает воп- рос о выгрузке уязвимых данных на диск, то для его разрешения потребуется безопасная опе- рационная система, запрещающая выгрузку данных на диск и переход в спящий режим.
1. Ограничивайте срок действия уязвимых данных 25 Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, информация об имени пользователя и пароле читается с консоли, после чего пароль сохраняется в объекте типа String. Учетные данные остаются открытыми до тех пор, пока система “сборки мусора” не очистит оперативную память, выделенную для хране- ния объекта типа String. class Password { public static void main (String args[]) throws lOException { Console c = System.console (); if (c == null) { System.err.printin ("No console.’’) ; System.exit(1); } String username = c.readLine("Enter your user name: "); String password = c.readLine ("Enter your password: "); if (!verify(username, password)) { throw new SecurityException ("Invalid Credentials’’); } // ... } // фиктивный метод проверки достоверности всегда возвращает // логическое значение true private static final boolean verify(String username, String password) { return true; } Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, вызывается метод Console. readPassword () для чтения пароля с консоли. class Password { public static void main (String args[]) throws lOException { Console c = System.console(); if (c == null) { System.err.printin("No console."); System.exit(1); } String username = c.readLine("Enter your user name: "); char[] password = c.readPassword("Enter your password: "); if (’verify(username, password)) { throw new SecurityException("Invalid Credentials"); } // очистить пароль
26 Глава 1 Безопасность Arrays.fill(password, ' ') ; } // фиктивный метод проверки достоверности всегда возвращает // логическое значение true private static final boolean verify(String username, char[] password) { return true; } } Метод Console. readPassword () позволяет возвратить пароль в виде последовательнос- ти символов, а не объекта типа String. Следовательно, программист может удалить пароль из массива, как только он будет использован. Этот метод запрещает также эхоотображение пароля обратно на консоль. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, объект типа Buf feredReader служит в качестве оболочки, в которую заключается объект типа InputStreamReader, чтобы организовать чтение уязвимых данных из файла. void readData() throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader( new Fileinputstream("file"))); // считать данные из файла String data = br.readLine(); } Метод Buf feredReader. readLine () возвращает уязвимые данные в виде объекта типа String, который еще долго остается в оперативной памяти после того, как хранящиеся в нем данные уже не нужны. А метод Buf feredReader. read (char [ ], int, int) считывает данные в заполняемый массив типа char. Но в этом случае программисту придется вручную очищать массив от уязвимых данных после их применения. С другой стороны, код будет стра- дать теми же самыми скрытыми на первый взгляд недостатками, даже если заключить объект типа Buf feredReader в оболочку объекта типа FileReader. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, для чтения уязвимых данных из файла используется непосредственно выделяемый буфер новой системы ввода-вывода (NIO). Данные могут быть очищены сразу же после их применения. Они не кешируются и не буферизуются в нескольких местах, а нахо- дятся только в системной памяти. Следует, однако, иметь в виду, что ручная очистка буфера в данном случае обязательна, поскольку “мусор” из непосредственно выделяемых буферов не собирается. void readData(){ ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024); try (FileChannel rdr = (new Fileinputstream("file")).getChannel()) { while (rdr.read(buffer) > 0) { // сделать что-нибудь с данными в буфере buffer.clear();
2. Не храните уязвимые данные незашифрованными на стороне клиента 27 } } catch (Throwable е) { // обработать ошибку Применимость Если не наложить ограничение на срок действия уязвимых данных, то может произойти их утечка. Библиография [API 2013] [Oracle 2013b] Class ByteBuf fer "Reading ASCII Passwords from an Inputstream Example" from the Java Cryptography Architecture [ JCA] Reference Guide [Tutorials 2013] I/O from the Command Line 2. Не храните уязвимые данные незашифрованными на стороне клиента Если приложение строится по модели “клиент-сервер”, то хранение уязвимых данных (на- пример, учетных данных пользователя) на стороне клиента может привести к их несанкци- онированному раскрытию, если клиент подвержен атакам. Самый распространенный способ устранения подобного недостатка в веб-приложениях состоит в том, чтобы предоставить клиенту cookie-файл, а уязвимые данные хранить на сервере. В этом случае cookie-файлы создаются на веб-сервере и хранятся на стороне клиента в течение определенного периода времени. Когда же клиент повторно подключается к серверу, он предоставляет cookie-файл, по которому клиент распознается на сервере, после чего сервер предоставляет ему уязвимые данные. Но cookie-файлы не защищают уязвимые данные от атак типа межсайтового выполнения сценариев (XSS). Злоумышленник, способный получить cookie-файл, произведя атаку типа XSS непосредственно на компьютер клиента, может извлечь уязвимые данные из сервера, исполь- зуя cookie-файл. Этот риск можно ограничить по времени, если сервер признает сеанс связи недействительным по истечении установленного лимита времени (например, 15 минут). Как правило, cookie-файл содержит короткую символьную строку. Если в ней содержит- ся конфиденциальная информация, то она должна быть зашифрована. К конфиденциальной, уязвимой информации относятся имена пользователей, пароли, номера кредитных карточек, номера карточек социального обеспечения и любые другие данные, идентифицирующие лич- ность пользователя. Подробнее о манипулировании паролями см. в рекомендации 13 “Храни- те пароли с помощью хеш-функции”, а о защите памяти, в которой хранится конфиденциаль- ная информация, — в рекомендации 1 “Ограничивайте срок действия уязвимых данных”. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, имя пользователя и пароль сохраняются сервлетом регистрации в cookie- файле, чтобы распознать пользователя при последующих запросах.
28 Глава 1 Безопасность protected void doPost (HttpServletRequest request, HttpServletResponse response) { // проверить достоверность введенных данных (пропущено) String username = request.getParameter("username"); char[] password = request.getParameter("password").toCharArray(); boolean rememberMe = Boolean.valueOf(request.getParameter("rememberme")); LoginService loginService = new LoginServicelmpl(); i f (r emembe rMe) { if (request .getCookies () [0] != null && request.getCookies()[0].getValue() != null) { String[] value = request.getCookies()[0].getValue().split(";"); if (!loginService.isUserValid(value[0], value[1].toCharArray())) { // установить код ошибки и возвратить } else { // перейти на страницу приветствия } } else { boolean validated = loginService.isUserValid(username, password); if (validated) { Cookie loginCookie = new Cookie("rememberme", username + + new String(password)); response.addCookie(loginCookie); // ... перейти на страницу приветствия } else { // установить код ошибки и возвратить } } } else { //ни одна из функциональных возможностей запоминания информации //не выбрана, поэтому продолжить обычную аутентификацию; // если она не пройдет, установить код ошибки и возвратить } Arrays.fill(password, ’ ’); } Но попытка реализовать функциональные возможности запоминания конфиденциальной информации в приведенном выше коде не является безопасной, поскольку злоумышленник, атакующий клиентский компьютер, может получить эту информацию непосредственно на стороне клиента. Кроме того, этот код нарушает рекомендацию 13 “Храните пароли с помо- щью хеш-функции”. Решение, соответствующее принятым нормам (сеанс связи) В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, функциональные возможности запоминания конфиденциальной ин- формации реализуются путем сохранения имени пользователя и безопасной произвольной символьной строки в cookie-файле. Кроме того, в объекте типа HttpSession сохраняется состояние сеанса связи. protected void doPost (HttpServletRequest request, HttpServletResponse response) { // проверить достоверность введенных данных (пропущено)
2. Не храните уязвимые данные незашифрованными на стороне клиента 29 String username = request.getParameter ("username") ; char[] password = request.getParameter("password").toCharArray(); boolean rememberMe = Boolean.valueOf(request.getParameter("rememberme")); LoginService loginService = new LoginServicelmpl(); boolean validated = false; i f (rememberMe) { if (request .getCookies () [0] != null && request.getCookies()[0].getValue() != null) { String[] value = request.getCookies()[0].getValue().split(";"); if (value.length != 2) { // установить код ошибки и возвратить } if (!loginService.mappingExists(value[0], value[1])) { // проверить пару (имя пользователя, произвольная строка) // установить код ошибки и возвратить } } else { validated = loginService.isUserValid(username, password); if ('validated) { // установить код ошибки и возвратить } } String newRandom = loginService.getRandomString(); // устанавливать каждый раз произвольную строку в исходное состояние loginService.mapUserForRememberMe(username, newRandom); HttpSession session = request.getSession(); session.invalidate(); session = request.getSession(true); // установить время истечения сеанса связи в течение 15 минут session.setMaxInactivelnterval(60 * 15); // сохранить атрибут пользователя и произвольный атрибут //в области действия сеанса связи session.setAttribute("user”, loginService.getUsername()); Cookie loginCookie = new Cookie("rememberme", username + ";" + newRandom); response.addCookie(loginCookie); II... перейти на страницу приветствия } else { // ни одна из функциональных возможностей запоминания // информации не выбрана, поэтому выполнить аутентификацию //с помощью метода isUserValid (), и если она не пройдет, // установить код ошибки } Arrays.fill(password, ' ’) ; } На сервере поддерживается сопоставление имен пользователей с безопасными произ- вольными символьными строками. Когда пользователь выбирает режим запоминания его конфиденциальной информации, в методе doPost () проверяется, содержится ли в предо- ставляемом cookie-файле пара, состоящая из достоверного имени пользователя и произволь- ной символьной строки. Если сопоставление выявит совпадающую пару, то сервер аутенти- фицирует пользователя и направит его на страницу приветствия, а иначе сервер возвратит
30 Глава 1 Безопасность клиенту ошибку. Если же пользователь выберет режим запоминания его конфиденциальной информации, но клиент не сможет предоставить достоверный cookie-файл, то сервер потре- бует аутентификации пользователя по его учетным данным. И если аутентификация пройдет успешно, то сервер выдаст новый cookie-файл с атрибутами запоминания конфиденциальной информации пользователя. Такое решение позволяет предотвратить атаки типа фиксации сеанса связи, делая недосто- верным текущий сеанс связи и организуя новый. Оно также сокращает временной промежу- ток, в течение которого злоумышленник может совершить атаку типа перехвата сеанса связи, устанавливая время истечения сеанса связи между последовательными доступами со стороны клиента равным 15 минутам. Применимость Если конфиденциальная информация сохраняется незашифрованной на стороне клиента, она становится доступной для всякого, совершающего атаку на компьютер клиента. Библиография [Oracle 2011c] [OWASP 2009] [OWASP 2011] [W3C 2003] Package javax. servlet. http Session Fixation in Java Cross-Site Scripting (XSS) The World Wide Web Security FAQ 3. Снабжайте уязвимые изменяемые классы немодифицируемыми оболочками Неизменяемость полей препятствует неумышленному их видоизменению, а также злона- меренному повреждению их содержимого, делая ненужным защитное копирование во время приема вводимых данных или возврата значений. Но в то же время некоторые уязвимые клас- сы нельзя сделать неизменяемыми. Правда, с помощью оболочек можно предоставить нена- дежному коду доступ к изменяемым классам только для чтения. Например, в состав классов типа Collection входит ряд оболочек, позволяющих клиентам просматривать неизменяе- мые представления объектов типа Collection. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, содержится класс Mutable, позволяющий видоизменять объект внутреннего массива. class Mutable { private int[] array = new int[10]; public int[] getArrayO { return array; } public void setArray(int[] i) { array = i; }
3. Снабжайте уязвимые изменяемые классы немодифицируемыми оболочками 31 } //... private Mutable mutable = new Mutable () ; public Mutable getMutableO {return mutable;} Из ненадежного кода может быть вызван модифицирующий метод setArray (), нарушив неизменяемость свойства объекта. Вызывая метод получения getArray (), можно также ви- доизменить закрытое внутреннее состояние класса. Такой класс нарушает также рекоменда- цию “OBJ05-J. Выполняйте защитное копирование закрытых изменяемых членов класса перед возвратом ссылок” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]. Пример кода, не соответствующего принятым нормам В следующем примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, класс Mutable расширяется подклассом MutableProtector. class MutableProtector extends Mutable { @Override public int[] getArray() { return super.getArray().clone (); } } // ... private Mutable mutable = new MutableProtector () ; // может быть благополучно вызван из ненадежного кода, // имеющего право на чтение public Mutable getMutableO {return mutable;} При вызове метода получения getArray () в этом классе не разрешается видоизменение закрытого внутреннего состояния класса в соответствии с рекомендацией “OBJ05-J. Выпол- няйте защитное копирование закрытых изменяемых членов класса перед возвратом ссылок” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]. Но в то же время из ненадежного кода можно вызвать метод setArray () и видоизменить объект типа Mutable. Решение, соответствующее принятым нормам Как правило, уязвимые классы могут быть преобразованы в безопасно просматриваемые объекты, для чего достаточно предоставить подходящие оболочки всем методам, определен- ным в базовом интерфейсе, в том числе и модифицирующим методам. Оболочки модифициру- ющих методов должны генерировать исключение типа UnsupportedOperationException, чтобы на стороне клиента можно было выполнять операции, оказывающие воздействие на свойство неизменяемости объекта. В представленном здесь решении, соответствующем при- нятым нормам безопасного программирования на Java, вводится метод setArray (), пере- определяющий метод Mutable. setArray () и предотвращающий видоизменение объекта типа Mutable. class MutableProtector extends Mutable { @Override public int[] getArray() { return super.getArray().clone();
32 Глава 1 Безопасность @Override public void setArray(int[] i) { throw new UnsupportedOperationException(); } // ... private Mutable mutable = new MutableProtector () ; // может быть благополучно вызван из ненадежного кода, // имеющего право на чтение public Mutable getMutableO {return mutable; } В оболочке класса типа MutableProtector переопределяется метод getArrayO и копируется массив. И хотя вызывающий код получает копию массива изменяемого объек- та, исходный массив остается неизменяемым и недоступным. При переопределении метода setArray () генерируется исключение, если в вызывающем коде предпринимается попытка воспользоваться этим методом для возвращаемого объекта. Ведь этот объект может быть пе- редан ненадежному коду, когда разрешается доступ к данным для чтения. Применимость Если не предоставить неизменяемое, безопасно просматриваемое представление уязвимо- го изменяемого объекта ненадежному коду, такой объект может быть злонамеренно повреж- ден и испорчен. Библиография Long 2012] OBJ05-J. Defensively copy private mutable class members before returning their references [Tutorials 2013] Unmodifiable Wrappers 4. Вызывайте уязвимые для безопасности методы с проверенными аргументами В прикладном коде, вызывающем уязвимые с точки зрения безопасности методы, должны быть проверены на достоверность аргументы, передаваемые этим методам. В частности, пус- тые значения null могут быть интерпретированы как допустимые некоторыми уязвимыми с точки зрения безопасности методами, но в то же время они способны переопределить уста- навливаемые по умолчанию значения. И хотя программирование уязвимых с точки зрения безопасности методов должно быть защитным, в клиентском коде должны быть проверены на достоверность аргументы, которые в противном случае могут быть приняты подобными методами как недостоверные. Если не выполнить такую проверку, то в конечном итоге это может привести к превышению полно- мочий и выполнению произвольного кода. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, демонстрируется вызов метода doPrivileged () с двумя аргументами, при- чем в качестве второго аргумента передается контекст управления доступом. В этом методе восстанавливаются права доступа из сохраненного прежде контекста.
5. Не допускайте выгрузку произвольных файлов 33 Accesscontroller.doPrivileged( new PrivilegedAction<Void>() { public Void run() { // ... } }, accessControlContext); Если методу doPrivileged (), принимающему два аргумента, передается пустой контекст управления доступом, он не в состоянии понизить полномочия до сохраненного прежде кон- текста. Следовательно, в рассматриваемом здесь коде могут быть предоставлены излишние полномочия, если аргумент accessControlContext принимает пустое значение null. Про- граммисты, намеревающиеся вызвать метод Accesscontroller. doPrivileged () с пустым контекстом управления доступом, должны передать явным образом пустую константу null или же вызвать вариант метода Accesscontroller. doPrivileged () с одним аргументом. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, предоставление излишних полномочий предотвращается благодаря тому, что обеспечивается непустое значение аргумента accessControlContext. if (accessControlContext == null) { throw new SecurityException("Missing AccessControlContext"); } Accesscontroller.doPrivileged( new PrivilegedAction<Void>() { public Void run() { // ... } }, accessControlContext); Применимость Методы, уязвимые с точки зрения безопасности, должны быть тщательно проанализи- рованы, а их параметры — проверены на достоверность, чтобы предотвратить появление непредвиденных значений аргументов (например, пустых). Если уязвимым с точки зрения безопасности методам передаются непредвиденные значения аргументов, то становится воз- можным выполнение произвольного кода, а также превышение полномочий. Библиография [API 2013] Accesscontroller. doPrivileged (), System. setSecurityManager () 5. He допускайте выгрузку произвольных файлов В приложениях на Java, в том числе и в веб-приложениях, принимающих выгружаемые фай- лы, должно быть гарантировано, что совершающий атаку злоумышленник не сможет выгрузить или передать злонамеренные файлы. Если файл с ограниченным доступом содержит код для выполнения в целевой системе, он способен нарушить ее защиту на прикладном уровне. На- пример, приложение, в котором разрешается выгружать HTML-файлы, может допустить вы- полнение злонамеренного кода, и в отсутствие процедуры экранирования выводимых данных
34 Глава 1 Безопасность совершающий атаку злоумышленник может предъявить достоверный HTML-файл с довеском вредоносного кода для межсайтового выполнения сценариев (XSS). Именно поэтому во мно- гих приложениях накладываются ограничения на типы файлов, которые можно выгружать. Вполне возможно, что будут выгружаться и файлы с опасными расширениями вроде . ехе и . sh, что может привести к выполнению произвольного кода в приложениях на стороне сер- вера. Приложение, накладывающее ограничение только на поле Content-Туре в НТТР-за- головке, может оказаться уязвимым к подобного рода нарушениям безопасности. Для подде- ржки выгрузки файлов обычная страница типа Java Server Pages (JSP — Серверные страницы на Java) содержит приведенный ниже код. <s:form action="doupload" method=”POST" enctype="multipart/form-data"> <s:file name="uploadFile’' label="Choose File" size="40" /> <s:submit value="Upload" name="submit" /> </s:form> Во многих каркасах приложений масштаба предприятия на Java предоставляются пара- метры настройки конфигурации, предназначенные в качестве защитной меры против про- извольной выгрузки файлов. К сожалению, большинство из них неспособно обеспечить над- лежащую защиту. Для устранения подобной уязвимости необходимо организовать проверку размера и содержимого файла, типа содержимого и прочих атрибутов метаданных. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, демонстрируется фрагмент XML-разметки выгружающего действия из приложения, простроенного на каркасе Struts 2. Код перехватчика отвечает за разрешение выгрузки файлов. <action name="doUpload" class="com.example.UploadAction"> <interceptor-ref name="fileupload"> <param name="maximumSize"> 10240 </param> <param name="allowedTypes"> text/plain,image/JPEG,text/html </param> </interceptor-ref> </action> Ниже приведен код из класса UploadAction для выгрузки файлов. public class UploadAction extends Actionsupport { private File uploadedFile; // метод доступа к файлу uploadedFile public String execute() { try { // путь к файлу и его имя жестко кодируются ради // большей наглядности примера File fileToCreate = new File("filepath", "filename"); // копировать в этот файл содержимое временного файла FileUtils.copyFile(uploadedFile, fileToCreate); return "SUCCESS"; } catch (Throwable e) {
5. Не допускайте выгрузку произвольных файлов 35 addActionError(e.getMessage()); return "ERROR"; } } } Значение параметра maximumsize гарантирует, что конкретное действие Action не полу- чит очень крупный файл. А параметр allowedTypes определяет допустимые типы файлов. Но такой подход совсем не гарантирует, что выгружаемый файл соответствует требованиям безопасности, поскольку проверки в перехватчике можно легко обойти. Если бы совершаю- щий атаку злоумышленник воспользовался инструментальным средством прокси-сервера для изменения типа содержимого в исходном HTTP-запросе при его пересылке, то каркас при- ложений не смог бы предотвратить выгрузку файла. Следовательно, злоумышленник мог бы выгрузить злонамеренный файл, например, с расширением . ехе. Решение, соответствующее принятым нормам Выгрузка файла должна пройти успешно лишь в том случае, если тип содержимого соот- ветствует конкретному содержимому файла. Например, файл с заголовком изображения дол- жен содержать только изображение, но не исполняемый код. В представленном здесь решении, соответствующем принятым нормам безопасного программирования на Java, для обнаруже- ния и извлечения метаданных и структурированного текстового содержимого из документов применяется библиотека Apache Tika [Apache 2013]. Метод checkMetaData () должен быть вызван перед вызовом метода execute (), непосредственно отвечающего за выгрузку фай- ла. А объект типа AutoDetectParser выбирает наилучший из доступных синтаксических анализаторов, исходя из типа содержимого файла, подвергаемого синтаксическому анализу. public class UploadAction extends Actionsupport { private File uploadedFile; // метод доступа к файлу uploadedFile public String execute() { try { // путь к файлу и его имя жестко кодируются ради // большей наглядности примера File fileToCreate = new File("filepath", "filename"); boolean textPlain = checkMetaData(uploadedFile, "text/plain"); boolean img = checkMetaData(uploadedFile, "image/JPEG"); boolean textHtml = checkMetaData(uploadedFile, "text/html"); if (ItextPlain || !img I I ItextHtml) { return "ERROR"; } // копировать в этот файл содержимое временного файла FileUtils.copyFile(uploadedFile, fileToCreate); return "SUCCESS"; } catch (Throwable e) { addActionError(e.getMessage()); return "ERROR"; } } public static boolean checkMetaData(File f, String getContentType) { try (Inputstream is = new Fileinputstream(f)) { ContentHandler contenthandler = new BodyContentHandler();
3€ Глава 1 Безопасность Metadata metadata = new Metadata () ; metadata.set(Metadata.RESOURCE_NAME_KEY, f.getName()); Parser parser = new AutoDetectParser (); try { parser.parse(is, contenthandler, metadata, new ParseContext()); } catch (SAXException | TikaException e) { // обработать ошибку return false; } if (metadata.get(Metadata.CONTENT_TYPE).equalsIgnoreCase( getContentType)) { return true; } else { return false; } } catch (lOException e) { // обработать ошибку return false; } } } Применимость Уязвимость выгрузки файлов может привести к превышению полномочий и выполнению произвольного кода. Библиография [Apache 2013] Apache Tika: A Content Analysis Toolkit б. Кодируйте или экранируйте выводимые данные надлежащим образом Надлежащая “санобработка” (т.е. очистка от всего лишнего) вводимых данных позволяет предотвратить внедрение злонамеренной информации в такую подсистему, как база данных. Но в разных системах требуются разные виды санобработки данных. Правда, в каждом кон- кретном случае, как правило, можно достоверно выяснить, какие именно данные вводятся в подсистему, а следовательно, выбрать подходящий вид их очистки. Для вывода данных имеется несколько подсистем. К числу наиболее распространенных от- носится средство воспроизведения HTML-разметки, предназначенное для отображения выво- димых данных. Данные, направляемые в подсистему вывода, могут происходить из надежного источника. Но было бы опрометчиво предположить, что “санобработка” выводимых данных вообще не нужна, поскольку такие данные могут косвенным образом происходить из нена- дежного источника, а следовательно, включать в себя злонамеренное содержимое. Если не вы- полнить надлежащую очистку данных, направляемых в подсистему вывода, возникнет угроза нескольких видов нарушения безопасности. Например, средства воспроизведения HTML-раз- метки подвержены атакам типа внедрения кода HTML и межсайтового выполнения сценариев (XSS) [OWASP 2011]. Таким образом, санобработка выводимых данных с целью предотвратить подобные атаки оказывается не менее важной, чем санобработка вводимых данных.
6. Кодируйте или экранируйте выводимые данные надлежащим образом 37 Аналогично проверке достоверности вводимых данных, выводимые данные должны быть нормализованы перед их очисткой от злонамеренных символов. Надлежащее кодирование всех выводимых символов, кроме достоверно безопасных, позволяет исключить уязвимости, обусловленные теми данными, которые способны обойти проверку достоверности. Подробнее об этом см. в рекомендации “IDSO1-J. Нормализуйте символьные строки перед проверкой их достоверности” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012]. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, шаблон проектирования MVC (модель-представление-контроллер) из каркаса приложений масштаба предприятия Spring Framework, построенного на основе вер- сии Java ЕЕ, используется для представления данных пользователю без предварительного кодирования или экранирования. Эти данные посылаются веб-браузеру, а следовательно, рассматриваемый здесь код подвержен атакам типа внедрения кода HTML и межсайтового выполнения сценариев. @RequestMapping ("/getnotifications .htm") public ModelAndView getNotifications ( HttpServletRequest request, HttpServletResponse response) { ModelAndView mv = new ModelAndView () ; try { Userinfo userDetails = getUserlnfo(); List<Map<String,Object» list = new ArrayList<Map<String, Object» (); List<Notification> notificationList = Notificationservice.getNotificationsForUserld( userDetails.getPersonld()); for (Notification notification: notificationList) { Map<String,Object> map = new HashMap<String, Object>(); map.put (’’id", notification.getld() ) ; map.put("message”, notification.getMessage ()); list.add(map); } mv.addObject("Notifications", list); } catch (Throwable t) { // зарегистрировать в файле и обработать } return mv; } Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, определяется класс ValidateOutput, нормализующий выводимые данные по известному набору символов, выполняющий их “санобработку” по белому списку и кодирующий любые незаданные значения данных, вводя в действие механизм двойной про- верки. Следует, однако, иметь в виду, что требуемые шаблоны для сопоставления с белыми списками могут изменяться в зависимости от конкретных потребностей очистки данных в разных полях [OWASP 2013].
38 Глава 1 Безопасность public class ValidateOutput { // допустить только буквенно-цифровые символы и пробелы private static final Pattern pattern = Pattern.compile ("A [a-zA-Z0-9\\s] {0,20}$”); // проверяет на достоверность и кодирует вводимые в поле данные, // исходя из конкретного белого списка public String validate(String name, String input) throws ValidationException { String canonical = normalize(input); if (!pattern.matcher(canonical).matches()) { throw new ValidationException ("Improper format in " + name + " field"); } // кодирует выводимые данные с недостоверными символами canonical = HTMLEntityEncode(canonical); return canonical; } // нормализует по известным образцам private String normalize(String input) { String canonical = java.text.Normalizer.normalize(input, Normalizer.Form.NFKC); return canonical; } // кодирует недостоверные данные private static String HTMLEntityEncode(String input) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < input. length (); i++) { char ch = input.charAt(i); if (Character.isLetterOrDigit(ch) I I Character.isWhitespace(ch)) { sb.append(ch); } else { sb.append("&#" + (int)ch + ";"); } } return sb.toStringO ; } } // ... @RequestMapping("/getnotifications.htm") public ModelAndView getNotifications(HttpServletRequest request, HttpServletResponse response) { ValidateOutput vo = new ValidateOutput(); ModelAndView mv = new ModelAndView (); try { Userinfo userDetails = getUserlnfo(); List<Map<String,Object» list = new ArrayList<Map<String,Object»(); List<Notification> notificationList = Notificationservice.getNotificationsForUserld( serDetails.getPersonldO );
6, Кодируйте или экранируйте выводимые данные надлежащим образом 39 for (Notification notification: notificationList) { Map<String,Object> map = new HashMap<String,Object>(); map.put("id", vo.validate("id” ,notification.getld())); map.put("message", vo.validate("message", notification.getMessage())); list.add(map); } mv.addObject("Notifications", list); } catch (Throwable t) { // зарегистрировать в файле и обработать } return mv; } Кодирование и экранирование выводимых данных является обязательным, когда в них присутствуют такие потенциально опасные символы, как двойные кавычки и угловые скоб- ки. Экранирование выводимых данных рекомендуется выполнять даже в тех случаях, когда вводимые данные проверены по белому списку, чтобы исключить из них подобные симво- лы, поскольку такая мера обеспечивает дополнительную степень защиты. Однако конкретная последовательность экранирования символов зависит от места вставки выводимых данных. Например, небезопасные выводимые данные могут оказаться в значении атрибута элемента HTML-разметки, таблице стилей CSS, URL или сценарии, и поэтому в каждом конкретном случае процедура кодирования выводимых данных может оказаться иной. В некоторых кон- текстах безопасное использование небезопасных данных оказывается практически невозмож- ным. Подробнее о предупреждении атак типа межсайтового выполнения сценариев см. в до- кументе “XSS (Cross-Site Scripting) Prevention Cheat Sheet” (Шпаргалка по предотвращению XSS — межсайтового выполнения сценариев), опубликованном в рамках проекта OWASP (Open Web Application Security Project — Открытый проект по безопасности веб-приложений) по адресу www.owasp.org/index.php/XSS_Prevention_Cheat_Sheet. Применимость Если не кодировать и не экранировать выводимые данные перед их отображением или передачей через границу доверия, то в конечном итоге это может привести к выполнению произвольного кода. Связанные уязвимости Уязвимость Apache GERONIMO-1474, о которой было сообщено в январе 2006 года, поз- воляла совершающим атаки злоумышленникам предъявлять URL, содержавшие сценарий JavaScript. Инструментальное средство для просмотра журналов регистрации доступа к веб- сайту (Web Access Log Viewer), входящее в состав веб-приложения Apache Geronimo, оказа- лось не в состоянии очистить данные, направляемые на консоль администратора, вследствие чего стали возможными классические атаки типа XSS. Библиография [Long 2012] IDS01 -J. Normalize strings before validating them [OWASP 2011] Cross-Site Scripting (XSS) [OWASP 2013] How to Add Validation Logic to HttpServletRequest XSS (Cross-Site Scripting) Prevention Cheat Sheet
40 Глава 1 Безопасность 7. Предотвращайте внедрение кода Внедрение кода может произойти в том случае, если небезопасные вводимые данные вно- сятся в динамически конструируемый код. К числу наиболее очевидных источников потенци- альной уязвимости в подобных случаях относится выполнение сценария JavaScript из кода на Java. В состав пакета javax. script входят интерфейсы и классы, определяющие механизмы сценариев Java и каркас для применения этих интерфейсов и классов в коде на Java. Вследс- твие неправильного применения прикладного интерфейса API из пакета javax. script со- вершающий атаку злоумышленник получает возможность выполнить произвольный код в целевой системе. Предлагаемая здесь рекомендация является частным случаем рекомендации “IDSOO-J. Производите очистку небезопасных данных, передаваемых через границу доверия” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012]. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, небезопасные данные, вводимые пользователем, внедряются в оператор JavaScript, отвечающий за вывод введенных данных на печать. private static void evalScript(String firstName) throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager () ; ScriptEngine engine = manager.getEngineByName("javascript"); engine.eval("print('"+ firstName + "’)"); } Совершающий атаку злоумышленник может ввести специально составленный аргумент, пытаясь внедрить злонамеренный сценарий JavaScript. В данном примере демонстрируется внедряемая со злым умыслом символьная строка, содержащая код JavaScript, способный со- здать или перезаписать существующий файл в уязвимой системе. Сценарий в данном приме- ре выводит сначала на печать символьную строку ’’dummy”, а затем записывает символьную строку "some text" в файл конфигурации config.cfg. Но вместо этого в конкретном слу- чае использования подобной уязвимости может быть выполнен произвольный код. dummy\’); var bw = new Javaimporter(java.io.BufferedWriter); var fw = new Javaimporter(java.io.FileWriter); with(fw) with(bw) { bwr = new BufferedWriter(new FileWriter(\"config.cfg\")); bwr.write(\"some text\"); bwr.close(); } // ; Решение, соответствующее принятым нормам (проверка по белому списку) Наилучший способ защиты от уязвимостей, обусловленных внедрением кода, состоит в том, чтобы предотвратить включение в прикладной код данных, которые вводятся пользова- телем и могут содержать исполняемый код. Данные, введенные пользователем и используемые в динамическом коде, должны пройти санобработку, чтобы содержать только достоверные
7. Предотвращайте внедрение кода 41 символы, выверенные по белому списку. Такую санобработку лучше всего выполнить сразу же после ввода данных с помощью методов из абстрактного представления, применяемого для хранения и обработки данных. Подробнее об этом см. в рекомендации “IDSOO-J. Произво- дите санобработку небезопасных данных, передаваемых через границу доверия” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]. Если в имени допускается ис- пользование специальных символов, они должны быть нормализованы перед сравнением с их эквивалентными формами для целей проверки достоверности вводимых данных. В представ- ленном здесь решении, соответствующем принятым нормам безопасного программирования на Java, проверка по белому списку используется с целью предотвратить интерпретацию вве- денных и не прошедших санобработку данных в механизме сценариев. private static void evalScript(String firstName) throws ScriptException { // разрешить применение в символьной строке firstName только // буквенно-цифровых символов и знаков подчеркивания // (видоизменить, если символьная строка firstName может // содержать и специальные символы) if (!firstName.matches("[\\w]*")) { // содержимое символьной строки не соответствует символам, // выверенным по белому списку throw new IllegalArgumentException(); } ScriptEngineManager manager = new ScriptEngineManager () ; ScriptEngine engine = manager.getEngineByName("javascript"); engine.eval("print(’"+ firstName + "’)"); } Решение, соответствующее принятым нормам (безопасная "песочница") Другой подход состоит в том, чтобы создать безопасную “песочницу” используя диспет- чер защиты (см. рекомендацию 20 “Создавайте безопасную “песочницу”, используя диспет- чер защиты”). В приложении должно быть запрещено выполнение произвольных команд по сценарию, например, для обращения к локальной файловой системе. Для понижения полно- мочий, когда с повышенными полномочиями должно работать приложение, но не механизм сценариев, можно воспользоваться вариантом метода doPrivileged () с двумя аргументами. Класс RestrictedAccessControlContext понижает полномочия, предоставляемые в ис- ходном файле правил защиты для работы во вновь созданной защищенной области. Эффек- тивными считаются такие полномочия, которые находятся на пересечении полномочий во вновь созданной защищенной области и правил защиты на уровне всей системы. Подробнее о варианте метода doPrivileged () с двумя аргументами см. в рекомендации 16 “Старайтесь не предоставлять излишние полномочия”. В представленном здесь решении, соответствую- щем принятым нормам безопасного программирования на Java, демонстрируется примене- ние класса AccessControlContext и форма метода doPrivileged () с двумя аргументами. Данный подход можно применять в сочетании с рассмотренной ранее проверкой по белому списку для обеспечения дополнительной безопасности. class ACC { private static class RestrictedAccessControlContext { private static final AccessControlContext INSTANCE;
42 Глава 1 Безопасность static { INSTANCE = new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, null) // без полномочии }); } } private static void evalScript (final String firstName) throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager () ; final ScriptEngine engine = manager.getEngineByName("javascript"); // ограничить полномочия, используя вариант метода doPrivileged() //с двумя аргументами try { Accesscontroller.doPrivileged( new PrivilegedExceptionAction<Object>() { public Object run() throws ScriptException { engine.eval("print('" + firstName + "’)"); return null; } }, //из вложенного класса RestrictedAccessControlContext.INSTANCE); } catch (PrivilegedActionException pae) { // обработать ошибку } } } Применимость Если не предотвратить внедрение кода, то в конечном итоге это может привести к выпол- нению произвольного кода. Библиография [API 2013] Package javax. script [Long 2012] IDSOO-J. Sanitize untrusted data passed across a trust boundary [OWASP 2013] Code Injection in Java 8. Предотвращайте внедрение операторов XPath Расширяемый язык разметки (XML — Extensible Markup Language) может быть исполь- зован для хранения данных таким же способом, как и в базе данных. Зачастую данные из- влекаются из XML-документа средствами языка XPath. Но когда информация предоставля- ется написанной на XPath процедуре извлечения данных из XML-документа без надлежащей санобработки, то возможно преднамеренное внедрение операторов XPath. Подобного рода нарушение безопасности сродни умышленному внесению запросов SQL или внедрению кода
8. Предотвращайте внедрение операторов XPath 43 XML-разметки (см. рекомендацию “IDSOO-J. Производите санобработку небезопасных данных» передаваемых через границу доверия” из стандарта The СЕКТ Oracle9 Secure Coding Standard for Java"' [Long 2012]). Совершающий атаку злоумышленник может ввести достоверные команды SQL или эле- менты XML-разметки в поля ввода данных составляемого запроса. В типичных случаях та- кого нарушения защиты условное поле составляемого запроса разрешает тавтологию, а иначе предоставляет совершающему атаку злоумышленнику доступ к привилегированной инфор- мации. Представленная здесь рекомендация является частным случаем более общей рекомен- дации 7 “Предотвращайте внедрение кода”. Пример внедрения операторов XPath Рассмотрим следующую схему разметки XML-документа: <users> <user> <username>Utah</username> <password>e90205372a3b89e2</password> </user> <user> <username>Bohdi</username> <pas sword>6c16b22 02 9df4 ec 6</pas sword> </user> <user> <username>Busey</username> <password>ad39b3c2a4dabc98</password> </user> </users> Пароли хешируются в соответствии с рекомендацией 13 “Храните пароли с помощью хеш-функции”. Хеш-коды, получаемые по алгоритму MD5, представлены в приведенном выше фрагменте разметки XML-документа лишь в целях иллюстрации, а на практике для тех же самых целей лучше применять более надежный алгоритм вроде SHA-256. Ненадеж- ный код может попытаться извлечь сведения о пользователе из данного документа с помо- щью оператора XPath, составляемого динамически из введенных пользователем данных, как показано ниже. //users/user[username/text()='&LOGIN&' and password/text()='&PASSWORD&' ] Если злоумышленнику станет известно, что Utah — достоверное имя пользователя, он может указать в вводимым данных следующее: Utah’ or ’l’=’l В итоге получится приведенная ниже символьная строка запроса. //users/user[username/text()='Utah' or and password/text()='xxxx'] А поскольку выражение ’ 1 ’ =11 ’ автоматически дает истинный результат, то достовер- ность пароля вообще не проверяется. Следовательно, злоумышленник будет неверно аутенти- фицирован как пользователь под именем Utah, даже не зная пароля этого пользователя.
44 Глава 1 Безопасность Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, имя пользователя и пароля читаются из введенных им данных, исходя из чего составляется символьная строка запроса. Пароль передается в качестве массива типа char, а затем хешируется. Этот пример демонстрирует уязвимость к описанному выше нару- шению безопасности. Если рассмотренная выше символьная строка, составленная соверша- ющим атаку злоумышленником, будет передана методу evaluate (), этот метод возвратит соответствующий узел из XML-файла, что в свою очередь приведет к возврату логического значения true из метода doLogin () в обход всякой аутентификации. private boolean doLogin(String userName, chart] password) throws ParserConfigurationException, SAXException, lOException, XPathExpressionException { DocumentBuilderFactory domFactory = DocumentBuilderFactory.newlnstance(); domFactory.setNamespaceAware(true); DocumentBuilder builder = domFactory.newDocumentBuilder(); Document doc = builder.parse("users.xml"); String pwd = hashPassword ( password) ; XPathFactory factory = XPathFactory.newlnstance(); XPath xpath = factory.newXPath(); XPathExpression expr = xpath.compile("//users/user[username/text () = ”’ + userName + "' and password/text () = ’" + pwd + ’” ]"); Object result = expr.evaluate(doc, XPathConstants.NODESET); NodeList nodes = (NodeList) result; // вывести имена на консоль for (int i = 0; i < nodes .getLength () ; i++) { Node node = nodes.item(i).getChildNodes().item(l).getChildNodes().item(0); System.out.println("Authenticated: " + node.getNodeValue()); } return (nodes.getLength() >= 1); } Решение, соответствующее принятым нормам (библиотека XQuery) Умышленное внедрение операторов XPath можно не допустить, приняв такие же меры за- щиты, как и при предотвращении умышленного внесения запросов SQL. Ниже перечислены эти меры защиты. Рассматривайте все вводимые пользователем данные как небезопасные, выполняя над- лежащую санобработку. Выполняя санобработку вводимых пользователем данных, проверяйте правильность типа, длины, формата и содержимого этих данных. Например, составьте регулярное выражение, проверяющее наличие дескрипторов XML-разметки и специальных симво- лов во вводимых пользователем данных. Эта практическая мера защиты соответству- ет санобработке вводимых данных, как дополнительно поясняется в рекомендации 7 “Предотвращайте внедрение кода”.
8. Предотвращайте внедрение операторов XPath 45 Выполняйте в приложении, написанном по модели “клиент-сервер”, проверку досто- верности данных как на стороне клиента, так и на стороне сервера. Тщательно тестируйте приложения, предоставляющие, распространяющие или прини- мающие введенные пользователем данные. Для предотвращения умышленного внесения запросов SQL применяется также эффек- тивный способ параметризации. Параметризация гарантирует, что указанные пользователем данные передаются прикладному интерфейсу API в виде параметра, а следовательно, они во- обще не интерпретируются как исполняемое содержимое. К сожалению, в версии Java SE в настоящее время отсутствует аналогичный прикладной интерфейс для запросов XPath. Тем не менее параметризацию запросов XPath аналогично запросам SQL можно сымитировать, ис- пользуя прикладной интерфейс библиотеки XQuery, где поддерживается указание оператора запроса в отдельном файле, предоставляемом во время выполнения. Входной файл: login.xq declare variable $userName as xs:string external; declare variable $password as xs:string external; //users/user[@userName=$userName and @password=$password] В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, используется запрос, указанный в текстовом файле. Он читается из файла в требуемом формате, а затем значения, определяющие имя пользователя и пароль, вводятся в объект типа Мар. На основании этих введенных данных средствами библиотеки XQuery составляется запрос в формате XML. Такое решение гарантирует, что данные, указан- ные в полях ввода имени пользователя и пароля, не могут быть интерпретированы во время выполнения как исполняемое содержимое. private boolean doLogin(String userName, String pwd) throws ParserConfigurationException, SAXException, lOException, XPathExpressionException { DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance (); domFactory.setNamespaceAware(true); DocumentBuilder builder = domFactory.newDocumentBuilder(); Document doc - builder.parse("users.xml"); XQuery xquery = new XQueryFactory().createXQuery(new File("login.xq")); Map queryVars = new HashMap(); queryVars.put("userName", userName); queryVars.put("password", pwd) ; NodeList nodes = xquery.execute(doc, null, queryVars).toNodes(); // вывести имена на консоль for (int i = 0; i < nodes .getLength () ; i++) { Node node = nodes.item(i).getChildNodes().item(l). getChildNodes().item(0); System.out.printIn(node.getNodeValue()); } return (nodes.getLength() >=1);
46 Глава 1 Безопасность Применимость Если не организовать проверку достоверности вводимых пользователем данных, то в ко- нечном итоге это может привести к раскрытию уязвимой информации и выполнению недоз- воленного кода. Согласно OWASP [OWASP 2013], для того чтобы предотвратить умышленное внедрение операторов XPath, необходимо принять следующие меры. Удалить (т.е. запретить применение) или надлежащим образом экранировать символы п, <, >, /, ’, = и ”, чтобы не допустить прямое внесение параметров. Очистить запросы XPath от любых метасимволов (например, и // и подоб- ных им символов). Очистить расширения XSLT от любых вводимых пользователем данных, а иначе тща- тельно проверить наличие соответствующих файлов и обеспечить их соответствие правилам безопасности, установленным в версии Java 2. Библиография [Fortify 2013] [Long 2012] [OWASP 2013] [Sen 2007] [Oracle 2011b] "Input Validation and Representation: XML Injection" IDSOO-J. Sanitize untrusted data passed across a trust boundary Testing for XPath Injection Avoid the Dangers of XPath Injection Ensure Data Security 9. Предотвращайте внедрение операторов LDAP Упрощенный протокол доступа к каталогам (Lightweight Directory Access Protocol — LDAP) позволяет выполнять в удаленном режиме работы приложения такие операции, как поиск и видоизменение записей в каталогах. Но умышленное внедрение операторов LDAP вследствие недостаточной санобработки и проверки достоверности вводимых данных позволяет злона- меренным пользователям собирать информацию ограниченного доступа, используя службу каталогов. Для ограничения вводимых данных только достоверными символами можно воспользо- ваться белыми списками. Из белых списков должны быть исключены отдельные символы и их последовательности, в том числе метасимволы прикладного интерфейса JNDI (Java Naming and Directory Interface — интерфейс именования и каталогов на Java), а также специальные символы, применяемые в протоколе LDAP. Все эти символы перечислены в табл. 1.1. Таблица 1.1. Отдельные символы и их последовательности, исключаемые из белых списков Символ Наименование • и ” Одиночные и двойные кавычки / и \ Прямая и обратная косая черта \\ Двойная обратная косая черта* Пробел Символ пробела в начале и в конце строки # Знак номера (или решетки) в начале и в конце строки < и > Угловые скобки
9. Предотвращайте внедрение операторов LDAP 47 Окончание табл. 1.1 Символ Наименование , и ; Запятая и точка с запятой + и * Знаки, обозначающие арифметические операции сложения и умножения ( и ) Круглые скобки \u0000 Пустой символ NULL в уникоде * Это последовательность символов. Пример внедрения операторов LDAP Рассмотрим файл формата LDIF (LDAP Data Interchange Format — формат обмена данны- ми по протоколу LDAP), содержащий записи в следующем виде: dn: dc=example,dc=com objectclass: dcobject objectclass: organization o: Some Name de: example dn: ou=People,dc=example,dc=com ou: People objectclass: dcobject objectclass: organizationalUnit de: example dn: cn=Manager,ou=People,dc=example,dc=com cn: Manager sn: John Watson # Некоторые определения класса objectclass здесь опущены userPassword: secretl mail: john@holmesassociates.com dn: cn=Senior Manager,ou=People,dc=example,dc=com cn: Senior Manager sn: Sherlock Holmes # Некоторые определения класса objectclass здесь опущены userPassword: secret2 mail: sherlock@holmesassociates.com Поиск достоверного имени пользователя и пароля обычно принимает следующую форму: (&(sn=<USERSN>)(userPassword=<USERPASSWORD>)) Тем не менее совершающий атаку злоумышленник может обойти аутентификацию, под- ставив символы S* в поле USERSN и знак * в поле USERPASSWORD. Введенные подобным образом данные позволяют получить все записи, поле USERSN которых начинается с сим- вола S. В итоге процедура аутентификации, допустившая внедрение операторов LDAP, поз- волит неуполномоченным пользователям войти в систему. Аналогично процедура поиска позволит совершающему атаку злоумышленнику раскрыть частично или полностью данные в каталоге.
48 Глава 1 Безопасность Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, показано, что из той части кода, где вызывается метод searchRecord (), можно осуществить поиск записи в каталоге по протоколу LDAP. Фильтр символьных строк служит для просеивания в результирующем множестве тех записей, которые совпадают с именем пользователя и паролем, указанными в вызывающем коде. // String userSN = "S*"; // недействительно // String user Password = // недействительно public class LDAPInjection { private void searchRecord(String userSN, String userPassword) throws NamingException { Hashtable<String, String> env = new Hashtable<String, String>(); env.put(Context.INITIAL J3ONTEXT_FACTORY, "com.sun.j ndi.Idap.LdapCtxFactorу"); try { DirContext dctx = new InitialDirContext(env); Searchcontrols sc = new Searchcontrols(); String[] attributeFilter = {"cn", "mail”}; sc.setReturningAttributes(attributeFilter); sc.setSearchScope(Searchcontrols.SUBTREE_SCOPE); String base = "dc=example,dc=com"; // следующая строка дает такой результат: (&(sn=S*) (userPassword=*) ) String filter = ”(&(sn=" + userSN + ’’) (userPassword=" + userPassword + "))"; NamingEnumeration<?> results = dctx.search (base, filter, sc); while (results.hasMore()) { SearchResult sr = (SearchResult) results.next(); Attributes attrs = (Attributes) sr.getAttributes (); Attribute attr = (Attribute) attrs.get("cn"); System.out.printin(attr); attr = (Attribute) attrs.get("mail"); System.out.printin(attr) ; } dctx.close(); } catch (NamingException e) { // направить обработчику ошибок } } } Когда злонамеренный пользователь введет специально составленные данные, как поясня- лось ранее, приведенная выше элементарная схема аутентификации окажется не в состоянии ограничить результаты поиска по запросу той информацией, на доступ к которой этот поль- зователь имеет права. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, для санобработки вводимых пользователем данных используется бе- лый список, чтобы отобрать символьную строку, содержащую только достоверные символы.
10. Не пользуйтесь методом cloneQ для копирования... 49 Так, в приведенном ниже примере кода строка userSN может содержать только буквенно- цифровые символы. // String userSN = "Sherlock Holmes"; // действительно // String userPassword = "secret2"; // действительно // ... начало метода LDAPInjection.searchRecord() . .. sc.setSearchScope(Searchcontrols.SUBTREE_SCOPE); String base = "dc=example, dc=com"; if (luserSN.matches("[\\w\\s]*") II !userPassword.matches("[\\w]*")) { throw new IHegalArgumentException ("Invalid input"); } String filter = "(&(sn = " + userSN + ") (userPassword=" + userPassword + ")) "; // ... остальная часть метода LDAPInjection.searchRecord() . . . Если поле базы данных (например, с паролем) должно содержать специальные символы, то подлинные данные очень важно сохранить в очищенном путем санобработки виде в базе данных, а также нормализовать любые введенные пользователем данные перед проверкой до- стоверности или сравнением. В отсутствие полной нормализации пользоваться символами, которые имеют специальное значение в прикладном интерфейсе JNDI и протоколе LDAP, не рекомендуется. Специальные символы должны быть преобразованы в очищенные путем санобработки безопасные значения, прежде чем они будут введены в выражение для проверки вводимых данных на достоверность по белому списку. Аналогично нормализация вводимых пользова- телем данных должна быть произведена до проверки на достоверность. Применимость Если не подвергнуть санобработке небезопасные вводимые данные, то в конечном итоге это может привести к раскрытию уязвимой информации и превышению полномочий. Библиография [OWASP 2013] Preventing LDAP Injection in Java 10. He пользуйтесь методом done() для копирования небезопасных параметров метода Создание защитных копий параметров модифицирующего метода уменьшает вероятность появления различных уязвимостей в защите. Подробнее об этом см. в рекомендации “OBJ06-J. Защитное копирование изменяемых входных параметров и внутренних компонентов” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012]. Тем не менее вследс- твие неправильного применения метода clone () совершающий атаку злоумышленник может воспользоваться уязвимостями, предоставив аргументы, которые на первый взгляд кажутся нормальными, но на самом деле приводят к возврату из метода неожиданных значений. Объ- екты, передаваемые в виде подобных аргументов, способны в конечном итоге обойти про- верку достоверности и контроль, предусмотренный системой безопасности. Когда в качестве
50 Глава 1 Безопасность аргумента методу передается объект некоторого класса, такой аргумент следует рассматривать как небезопасный и не пользоваться методом clone (), предоставляемым этим классом. Не следует также пользоваться методом clone () неконечных классов для создания защитных копий. Предлагаемая здесь рекомендация является частным случаем рекомендации 15 “Не по- лагайтесь на методы, которые могут быть переопределены в ненадежном коде.” Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, определяется метод validateValue () для проверки достоверности задава- емого момента времени. private Boolean validateValue(long time) { // выполнить проверку достоверности return true; // заданный момент времени действительный } private void storeDatelnDB(java.util.Date date) throws SQLException { final java.util.Date copy = (java.util.Date)date.clone(); if (validateValue(copy.getTime())) { Connection con = DriverManager.getConnection( "jdbc:microsoft:sqlserver://<HOST>:1433", "<UID>", "<PWD>" ); PreparedStatement pstmt = con.prepareStatement("UPDATE ACCESSDB SET TIME = ?"); pstmt.setLong(1, copy.getTime ()); // ... } } Метод storeDatelnDB () принимает небезопасный аргумент date и пытается сделать за- щитную копию, используя свой метод clone (). Это дает совершающему атаку злоумышлен- нику возможность взять под контроль выполнение программы, создав злонамеренный класс даты, расширяющий класс Date. Если злоумышленнику удастся выполнить свой код с теми же самыми полномочиями, что и у метода storeDatelnDB (), он, по существу, внедрит свой зловредный код в метод clone (), как показано ниже. class MaliciousDate extends java.util.Date { @0verride public MaliciousDate clone () { // здесь следует зловредный код } } Но даже если злоумышленнику удастся лишь предоставить зловредную дату с меньшими привилегиями, то и в этом случае он сумеет обойти проверку достоверности, нарушив нор- мальную работу остальной части программы. Рассмотрим следующий пример кода: public class MaliciousDate extends java.util.Date { private static int count = 0; @Override public long getTime() {
10. Не пользуйтесь методом doneQ для копирования... 51 java.util.Date d = new java.util.Date(); return (count++ == 1) ? d.getTime() : d.getTimeO - 1000; 1 } При первом вызове метода get Time () зловредная дата оказывается неопасным объектом даты, и это позволяет обойти проверку достоверности в методе storeDatelnDB (). Но в базе данных фактически будет запомнен неверно заданный момент времени. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, исключается применение метода clone (). Вместо этого создается но- вый объект типа java.util.Date, применяемый затем для проверки управления доступом и ввода в базу данных. private void storeDatelnDB(java.util.Date date) throws SQLException { final java.util.Date copy = new java.util.Date(date.getTime()); if (validateValue(copy.getTime())) { Connection con = DriverManager.getConnection( ”jdbc:microsoft:sqlserver://<HOST>:1433”, "<UID>”, ”<PWD>" ); PreparedStatement pstmt = con.prepareStatement("UPDATE ACCESSDB SET TIME = ?"); pstmt.setLong(l, copy.getTime ()); // ... } } Пример кода, не соответствующего принятым нормам (CVE-2012-0507) В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, демонстрируется конструктор базового класса AtomicReferenceArray, появившегося в обновлении 2 версии Java 1.7.0. public AtomicReferenceArray(Е[] array) { // конечное поле гарантирует видимость this.array = array.clone(); } Этот код был впоследствии вызван при компьютерном вторжении в виде “троянского коня” типа Flashback, заразившего 600 тысяч компьютеров Macintosh в апреле 1212 года1. Решение, соответствующее принятым нормам (CVE-2012-0507) В обновлении 3 версии Java 1.7.0 упомянутый выше конструктор был видоизменен таким образом, чтобы вместо метода clone () вызывался метод Arrays. copyOf (): 1 См. статью “Exploiting Java Vulnerability CVE-2012-0507 Using Metasploit” (Использование уязвимости CVE-2012-0507 в Java с помощью Metasploit), опубликованную пользователем BreakTheSec на сайте Slideshare, net 14 июля 2012 г. (www.slideshare.net/BreakTheSec/exploiting-java-vulnerability).
52 Глава 1 Безопасность public AtomicReferenceArray(Е[] array) { // конечное поле гарантирует видимость this, array = Ar rays. copyOf (array, array, length, Object[].class); } Применимость Применение метода clone () для копирования небезопасных аргументов дает совершаю- щим атаки злоумышленникам возможность выполнить произвольный код. Библиография [Long 2012] OBJ06-J. Defensively copy mutable inputs and mutable internal components [Sterbenz 2006] Secure Coding Antipatterns: Avoiding Vulnerabilities 11. He пользуйтесь методом Object. equals () для сравнения ключей шифрования По умолчанию метод java. lang.Object.equals () не способен сравнивать такие со- ставные объекты, как, например, ключи шифрования. В большинстве классов типа Key не предоставляется реализация метода equals (), переопределяющая метод Object.equals (). В подобных случаях компоненты составного объекта должны сравниваться по отдельности, чтобы гарантировать их правильность. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, два ключа сравниваются методом equals (). Сравниваемые ключи могут оказаться не равными, даже если они представляют одно и то же значение. private static boolean keysEqual(Key keyl, Key key2) { if (keyl.equals(key2)) { return true; } return false;} Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, для первой проверки сначала используется метод equals (), а затем сравниваются закодированные варианты ключей, чтобы упростить режим работы, не за- висящий от конкретного поставщика. При этом проверяется, представляют ли ключи типа RSAPrivateKey и RSAPrivateCrtKey эквивалентные секретные ключи [Oracle 2011b]. private static boolean keysEqual(Key keyl, Key key2) { if (keyl.equals(key2)) { return true; } if (Arrays.equals(keyl.getEncoded(), key2.getEncoded())) {
12. Не пользуйтесь небезопасными или слабыми алгоритмами шифрования 53 return true; // здесь следует дополнительный код для других типов ключей, // например, в приведенном ниже коде можно проверить, равны ли / / ключи типа RSAPrivateKey и RSAPrivateCrtKey if ((keyl instanceof RSAPrivateKey) && (key2 instanceof RSAPrivateKey)) { if ((((RSAKey) keyl).getModulus().equals( ((RSAKey) key2).getModulus())) && (((RSAPrivateKey) keyl).getPrivateExponent().equals ( ((RSAPrivateKey) key2).getPrivateExponent ()))) { return true; } } return false; Автоматическое обнаружение Применение метода Object .equals () для сравнения ключей шифрования может при- вести к неожиданным результатам. Библиография [API 2013] java. lang. Object. equals (), Object. equals () [Oracle 2011 b] Determining If Two Keys Are Equal (JCA Reference Guide) 12. He пользуйтесь небезопасными или слабыми алгоритмами шифрования В приложениях, где широко применяются меры безопасности, следует избегать употреб- ления небезопасных или слабых алгоритмов шифрования. Вычислительные мощности сов- ременных компьютеров позволяют раскрыть методом грубой силы данные, зашифрованные подобными алгоритмами. Например, алгоритм DES (Data Encryption Standard — стандарт на шифрование данных) считается весьма ненадежным. В частности, сообщения, зашифрован- ные по этому алгоритму, были расшифрованы методом грубой силы в течение одного дня на таких компьютерах, как, например, Deep Crack (Глубокий раскол), в правозащитной органи- зации Electronic Frontier Foundation (EFF — Фонд электронных рубежей). Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, вводимые данные типа String шифруются по слабому алгоритму шиф- рования DES. SecretKey key = KeyGenerator.getlnstance("DES").generateKey(); Cipher cipher = Cipher.getlnstance("DES"); cipher.init(Cipher.ENCRYPT_MODE, key) ; // закодировать байты в коде UTF8; объект strToBeEncrypted содержит
54 Глава 1 Безопасность // входную символьную строку, которая должна быть зашифрована byte[] encoded = strToBeEncrypted.getBytes("UTF8"); // выполнить шифрование byte[] encrypted = cipher.doFinal(encoded); Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, для шифрования данных применяется более надежный и безопасный алгоритм AES (Advanced Encryption Standard — Усовершенствованный стандарт шифрования). Cipher cipher = Cipher.getlnstance("AES”); KeyGenerator kgen = KeyGenerator.getlnstance("AES"); kgen.init(128); // 192 и 256 битов может не поддерживаться SecretKey skey = kgen.generateKey(); byte[] raw = skey.getEncoded(); SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); cipher.init(Cipher.ENCRYPT_MODE, skeySpec); // закодировать байты в коде UTF8; объект strToBeEncrypted содержит // входную символьную строку, которая должна быть зашифрована byte[] encoded = StrToBeEncrypted.getBytes("UTF8"); / / выполнить шифрование byte[] encrypted = cipher.doFinal(encoded); Применимость Применение ненадежных в математическом и вычислительном отношении алгоритмов шифрования может привести к раскрытию уязвимых данных. Слабые алгоритмы шифрова- ния могут быть запрещены в версии Java SE 7. См. приложение D “Disabling Cryptographic Algorithms” (Запрет алгоритмов шифрования) к Java™ PKIProgrammers Guide (Руководство по программированию на Java в инфраструктуре открытых ключей) [Oracle 2011а]. Слабые алгоритмы шифрования могут применяться в тех случаях, когда специально тре- буется раскрываемый шифр. Например, шифр ROT 13 широко применятся в электронных до- сках объявлений и на веб-сайтах, где шифрование преследует цель защитить людей от инфор- мации, а не информацию от людей. Библиография [Oracle 2011 a] Appendix D, "Disabling Cryptographic Algorithms" [Oracle 2013b] Java Cryptography Architecture (JCA) Reference Guide 13. Храните пароли с помощью хеш-функции В тех программах, где пароли хранятся открытым текстом (т.е. в виде незашифрованных текстовых данных), существует риск раскрытия паролей самыми разными способами. И хотя
13. Храните пароли с помощью хеш-функции 55 пароли, как правило, принимаются от пользователей открытым текстом, в программах долж- ны быть приняты соответствующие меры, чтобы не хранить пароли открытым текстом. К числу наиболее подходящих средств для снижения вероятности раскрытия паролей от- носится применение хеш-функций, которые позволяют косвенным образом сравнивать в про- граммах вводимый пароль с символьной строкой исходного пароля, не сохраняя его откры- тым текстом или в незашифрованном виде. Такой подход сводит к минимуму риск раскрытия пароля практически без каких-либо существенных недостатков. Хеш-функции для шифрования Значение, выдаваемое хеш-функцией, называется хеш-значением, или сверткой сообще- ния. Хеш-функции являются осуществимыми в вычислительном отношении функциями, тог- да как обратные им функции неосуществимы в том же самом вычислительном отношении. На практике пароль может быть закодирован хеш-значением, но его декодирование остается неосуществимым. Поэтому равенство паролей может быть проверено по равенству их хеш- значений. С практической точки зрения хешируемый пароль рекомендуется всегда дополнять за- травкой — однозначным (нередко последовательно или произвольно) формируемым фраг- ментом данных, хранящимся вместе с хеш-значением. Применение затравки позволяет пре- дотвратить раскрытие хеш-значения методом грубой силы, при условии, что сама затравка оказывается достаточно длинной для получения требуемой энтропии (чем короче затравоч- ные значения, тем в меньшей степени они способны замедлить раскрытие хеш-значения ме- тодом грубой силы). У каждого пароля должна быть своя затравка. Если же одна затравка применяется для шифрования нескольких паролей, то два пользователя могут обнаружить, что у них одинаковые пароли. Выбор хеш-функции и длины затравки представляет собой определенного рода компро- мисс между безопасностью и производительностью. Так, можно выбрать более строгую хеш- функцию, чтобы для раскрытия пароля методом грубой силы пришлось приложить больше усилий, но в то же время для проверки достоверности пароля потребуется больше времени. Увеличение длины затравки также затрудняет раскрытие пароля методом грубой силы, но и требует дополнительного места для хранения затравки. В классе MessageDigest языка Java предоставляются реализации различных хеш-функций шифрования. Избегайте применения таких дефектных функций хеширования, как, например, по алгоритму MD5 (Message-Digest Algorithm — алгоритм вычисления свертки сообщения). А функции хеширования по алгоритму SHA-1 или SHA-2 (Secure Hash Algorithm — алгоритм безопасного хеширования) поддерживаются Агентством национальной безопасности США (National Security Agency) и в настоящее время считаются надежными. На практике во мно- гих приложениях применяются функции хеширования по алгоритму SHA-256, поскольку они обеспечивают приемлемую производительность и в то же время считаются достаточно на- дежными. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, пароль, сохраняемый в файле password.bin, зашифровывается и расшиф- ровывается по алгоритму с симметричным ключом. public final class Password { private void setPassword(byte[] pass) throws Exception {
56 Глава 1 Безопасность // произвольный алгоритм шифрования bytes[] encrypted = encrypt(pass); clearArray(pass); // сохранить зашифрованный пароль в файле password.bin saveBytes (encrypted, ’’password.bin”) ; clearArray(encrypted); } boolean checkPassword(byte[] pass) throws Exception { / / загрузить зашифрованный пароль byte[] encrypted = loadBytes("password.bin") ; byte[] decrypted = decrypt (encrypted) ; boolean arraysEqual = Arrays.equal(decrypted, pass); clearArray(decrypted); clearArray(pass); return arraysEqual; } private void clearArray(byte[] a) { for (int i = 0; i < a.length; i++) { a[i] = 0; } } } Совершающий атаку злоумышленник может расшифровать содержимое файла password.bin, особенно если ему известны ключ и алгоритм шифрования, применяемые в программе. Пароли должны быть защищены даже от системных администраторов и привиле- гированных пользователей. Следовательно, для снижения угроз раскрытия паролей шифрова- ние оказывается эффективным лишь отчасти. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, для сравнения хеш-значений вместо символьных строк открытым текстом применяется функция хеширования по алгоритму SHA-256 из класса MessageDigest, но для сохранения пароля применяется объект типа String. import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public final class Password { private void setPassword(String pass) throws Exception { byte[] salt = generateSalt(12); MessageDigest msgDigest = MessageDigest.getlnstance("SHA-256"); // зашифровать символьную строку с затравкой byte[] hashVal = msgDigest.digest((pass+salt).getBytesO); saveBytes(salt, "salt.bin"); // сохранить хеш-значение в файле password.bin saveBytes(hashVal,"password.bin"); } boolean checkPassword(String pass) throws Exception { byte[] salt = loadBytes("salt.bin"); MessageDigest msgDigest = MessageDigest.getlnstance("SHA-256");
13. Храните пароли с помощью хеш-функции 57 // зашифровать символьную строку с затравкой byte[] hashVall = msgDigest.digest((pass+salt).getBytes()); // загрузить хеш-значение из файла password.bin byte[] hashVa!2 = loadBytes("password.bin"); return Arrays.equals(hashVall, hashVal2); } private byte[] generateSalt(int n) { // сформировать массив произвольных байтов длиной п } } Даже если совершающему атаку злоумышленнику известно, что пароли хешируются в про- грамме по алгоритму SHA-256 с 12-разрядной затравкой, он все равно не сможет извлечь кон- кретный пароль из файлов password.bin и salt.bin. И хотя это более надежный пример шифрования паролей, чем предыдущий, тем не менее пароли могут неумышленно храниться в оперативной памяти открытым текстом. Ведь объекты типа String в языке Java являются неизменяемыми и могут копироваться и сохраняться внутренним механизмом работы вирту- альной машины Java. Следовательно, в Java отсутствует механизм для безопасного стирания пароля после его сохранения в объекте типа String. Подробнее об этом см. в рекомендации 1 “Ограничивайте срок действия уязвимых данных”. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, недостатки, выявленные в предыдущем примере кода, не соответству- ющего принятым нормам, устраняются благодаря применению массива байтов для хранения пароля. import j ava.security.MessageDigest; import j ava.security.NoSuchAlgorithmException; public final class Password { private void setPassword(byte[] pass) throws Exception { byte[] salt = generateSalt (12); byte[] input = appendArrays(pass, salt); MessageDigest msgDigest = MessageDigest.getlnstance("SHA-256"); // зашифровать символьную строку с затравкой byte[] hashVal = msgDigest.digest(input); clearArray(pass); clearArray(input); saveBytes(salt, "salt.bin"); // сохранить хеш-значение в файле password.bin saveBytes(hashVal,"password.bin"); clearArray(salt); clearArray(hashVal); } boolean checkPassword(byte[] pass) throws Exception { byte[] salt = loadBytes("salt.bin"); byte[] input = appendArrays(pass, salt); MessageDigest msgDigest = MessageDigest.getlnstance("SHA-256"); // зашифровать символьную строку с затравкой
58 Глава 1 Безопасность byte[] hashVall = msgDigest.digest(input); clearArray(pass); clearArray(input); // загрузить хеш-значение из файла password.bin byte[] hashVal2 = loadBytes("password.bin”); boolean arraysEqual = Arrays.equals(hashVall, hashVal2); clearArray(hashVall); clearArray(hashVal2); return arraysEqual; } private byte[] generateSalt(int n) { // сформировать массив произвольных байтов длиной п private byte[] appendArrays(byte[] a, byte[] b) { // возвратить новый массив a[], присоединенный к массиву b[] } private void clearArray (byte [ ] a) { for (int i = 0; i < a.length; i++) { a[i] = 0; } В обоих методах, set Pas sword () и checkPassword (), представление пароля открытым текстом стирается сразу же после его преобразования в хеш-значение. Следовательно, совер- шающим атаки злоумышленникам придется немало потрудиться, чтобы извлечь пароль от- крытым текстом после его стирания. Но обеспечить гарантированное стирание очень трудно, если вообще возможно, поскольку это зависит от конкретной платформы, применяемой сис- темы “сборки мусора”, динамического режима страничного обмена и прочих средств, которые действуют на более низком, чем язык Java, уровне. Применимость Пароли, сохраняемые без надежного хеширования, могут быть раскрыты злонамеренны- ми пользователями. Несоблюдение представленной здесь рекомендации обычно приводит к явному использованию связанной с этим уязвимости. В таких приложениях, как, например, диспетчеры паролей, возможно, потребуется извлечь исходный пароль, чтобы ввести его в стороннее приложение. И хотя это разрешено, тем не менее противоречит данной рекомен- дации. Диспетчер паролей доступен единственному пользователю и всегда имеет полномочия на сохранение паролей этого пользователя и их отображение по команде. Следовательно, фак- тором, ограничивающим безопасность и надежность, оказывается компетентность пользова- теля, а не порядок работы самой программы. Библиография [API 2013] Class MessageDigest Class String [Hirondelle 2013] [OWASP 2012] [Paar 2010] Passwords Never Clear in Text "Why Add Salt?" Chapter 11, "Hash Functions"
14. Обеспечьте подходящее начальное случайное значение... 59 14. Обеспечьте подходящее начальное случайное значение для класса SecureRandom Генерирование случайных чисел зависит от таких источников энтропии, как сигналы, устройства или аппаратные средства ввода данных. Безопасное генерирование случайных чисел рассматривается также в рекомендации “MSC02-J. Генерируйте надежные случайные числа” из стандарта The CERT* Oracle* Secure Coding Standard for Java"' [Long 2012]. Класс java, security.SecureRandom широко применяется для генерирования надежных с точки зрения шифрования случайных чисел. В файле java. security, который хранится в папке lib/security среды выполнения Java, предписывается следующее [API 2013]. Для генерирования случайных данных должен быть выбран подходящий источник. По умолчанию предпринимается попытка воспользоваться устройством накопления энт- ропии, указанным в свойстве securerandom. source. Если при доступе по заданно- му URL возникает исключение, то применяется традиционный алгоритм активизации потоков в системе. В операционных системах Solaris и Linux по умолчанию активизируется специальная реализация класса SecureRandom, если существует источник, указанный в следующе- му URL: file: /dev/urandom. В этом случае класс типа NativePRNG считывает про- извольные байты из источника по следующему URL: /dev/urandom. А в операцион- ных системах Windows можно воспользоваться функциями для получения начальных случайных значений из прикладного интерфейса CryptoAPI от корпорации Microsoft по следующим URL: f ile: /dev/random и f ile: /dev/urandom. Злоумышленник не должен суметь определить исходно заданные начальные значения из нескольких выборок случайных чисел. Если же это ограничение нарушается, то все последую- щие случайные числа могут быть успешно предугаданы злоумышленником. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, конструируется генератор случайных чисел, который инициируется указан- ными байтами начального значения. SecureRandom random = new SecureRandom( String.valueOf(new Date().getTime()).getBytes() ); В приведенном выше конструкторе осуществляется поиск реестра поставщиков услуг по обеспечению безопасности и возвращается первый же поставщик, поддерживающий безо- пасное генерирование случайных чисел. Если же такой поставщик отсутствует, то выбира- ется вариант по умолчанию в зависимости от конкретной реализации. Более того, начальное случайное значение, снабжаемое системой по умолчанию, заменяется начальным случайным значением, предоставляемым программистом. Но ведь выбор текущего системного времени в качестве начального случайного значения вполне предсказуем и может привести к генериро- ванию случайных чисел с недостаточной энтропией.
60 Глава 1 Безопасность Решение, соответствующее принятым нормам Предпочтение следует отдавать конструктору класса SecureRandom без аргументов, в ко- тором предоставляемое системой начальное случайное значение используется для генериро- вания произвольных чисел длиной 128 байт. Для лучшей переносимости прикладного кода целесообразно также указать конкретный генератор случайных чисел и поставщика услуг по обеспечению безопасности. byte[] randomBytes = new byte[128]; SecureRandom random = new SecureRandom(); random.nextBytes(randomBytes); Применимость Недостаточно надежные случайные числа дают совершающим атаки злоумышленникам возможность получить конкретные сведения о контексте, в котором они используются. Хотя небезопасные случайные числа оказываются полезными в некоторых контекстах, не требую- щих соблюдения мер безопасности. О таких числах упоминается в исключениях из рекомен- дации “MSC02-J. Генерируйте надежные случайные числа”, представленной в стандарте The CERT Oracle9 Secure Coding Standard for Java™ [Long 2012]. Библиография [API 2013] SecureRandom [Sethi 2009] Proper Use of Java's SecureRandom [Long 2012] MSC02-J. Generate strong random numbers 15. He полагайтесь на методы, которые могут быть переопределены в ненадежном коде В ненадежном коде могут быть неверно использованы прикладные интерфейсы API, пре- доставляемые безопасным кодом для переопределения таких методов, как Object. equals (), Obj ect. hashCode () и Thread. run (). Эти методы служат удобными мишенями для нару- шения безопасности, поскольку они обычно вызываются подспудно и могут взаимодейство- вать с компонентами трудно выявляемым образом. Предоставляя переопределяемые варианты этих методов, совершающий атаку злоумыш- ленник может воспользоваться ненадежным кодом для сбора секретной информации, вы- полнения произвольного кода или начала атаки типа отказа в обслуживании. Подробнее о переопределении метода Object.clone () см. в рекомендации 10 “Не пользуйтесь методом clone () для копирования небезопасных параметров метода”. Пример кода, не соответствующего принятым нормам (переопределение метода hashCode ()) В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, демонстрируется применение класса LicenseManager, поддерживающе- го отображение licenseMap. В этом отображении хранится пара, состоящая из ключа типа LicenseType и значения лицензии.
15. Не полагайтесь на методы, которые могут быть переопределены... 61 public class LicenseManager { Map<LicenseType, String> licenseMap = new HashMap<LicenseType, String>(); public LicenseManager() { LicenseType type = new LicenseType(); type. setType ("demo-license-key") ; licenseMap.put(type, "ABC-DEF-PQR-XYZ"); } public Object getLicenseKey(LicenseType licenseType) { return licenseMap.get(licenseType); } public void setLicenseKey(LicenseType licenseType, String licenseKey) { licenseMap.put(licenseType, licenseKey); } } class LicenseType { private String type; public String getTypeO { return type; } public void setType(String type) { this, type = type; } ©Override public int hashCode() { int res = 17; res = res * 31 + type == null ? 0 : type.hashCode(); return res; } ©Override public boolean equals(Object arg) { if (arg == null I | ! (arg instanceof LicenseType) ) { return false; } if (type.equals(((LicenseType) arg).getType())) { return true; } return false; } } Конструктор класса LicenseManager инициализирует отображение licenseMap клю- чом лицензии на демонстрацию, который должен оставаться в секрете. Ключ лицензии жестко кодируется в целях иллюстрации, но в идеальном случае он должен быть прочи- тан из внешнего файла конфигурации, в котором ключ хранится в зашифрованном виде. В классе LicenseType предоставляются переопределяемые варианты методов equals () и hashCode (). Но такая реализация уязвима для атаки злоумышленника, который может рас- ширить класс LicenseType и переопределить методы equals () и hashCode (), как показа- но ниже. public class CraftedLicenseType extends LicenseType { private static int guessedHashCode = 0;
62 Глава 1 Безопасность @Override public int hashCode () { // возвратить новый хеш-код для проверки всякий раз, // когда вызывается метод get () guessedHashCode++; return guessedHashCode; } @Override public boolean equals(Object arg) { // всегда возвращать логическое значение true return true; } } Ниже приведен исходный код зловредной клиентской программы. public class DemoClient { public static void main (String[] args) { LicenseManager licenseManager = new LicenseManager(); for (int i = 0; i <= Integer.MAX_VALUE; i++) { Object guessed = licenseManager.getLicenseKey(new CraftedLicenseType()); if (guessed != null) { // вывести ABC-DEF-PQR-XYZ System.out.printin(guessed); } } } } Клиентская программа подбирает все возможные хеш-коды, используя объект типа CraftedLicenseType до тех пор, пока не обнаружит успешное совпадение с хеш-кодом ключа лицензии на демонстрацию, хранящегося в виде объекта в классе LicenseManager. Следовательно, совершающий атаку злоумышленник может в считанные минуты обнаружить секретные данные в отображении licenseMap. В ходе подобной атаки обнаруживается хотя бы один хеш-конфликт по отношению к ключу из отображения. Решение, соответствующее принятым нормам (отображение типа IdentityHashMap) В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, для хранения сведений о лицензии применяется отображение типа IdentityHashMap, а не типа HashMap. public class LicenseManager { Map<LicenseType, String> licenseMap = new IdentityHashMap<LicenseType, String>(); // ... } Согласно документации на класс IdentityHashMap в прикладном интерфейсе API языка Java [API 2006], в этом классе реализуется интерфейс Мар с хеш-таблицей, где для сравнения ключей (и значений) используется равенство ссылок, а не объектов. Иными словами, в классе IdentityHashMap два ключа, kl и к2, считаются равными в том и только в том случае, если
15. Не полагайтесь на методы, которые могут быть переопределены... 63 kl==k2. (В обычных реализациях интерфейса Мар (вроде HashMap) два ключа считаются рав- ными в том и только в том случае, если (kl==null ? k2==null : kl.equals (k2)).) Следовательно, переопределяемые методы не смогут раскрыть подробности внутренне- го устройства класса. Клиентская программа может и далее вводить ключи лицензии и даже извлекать введенные пары “ключ-значение”, как показано в приведенном ниже клиентском коде. public class DemoClient { public static void main(String[] args) { LicenseManager licenseManager = new LicenseManager(); LicenseType type = new LicenseType(); type.setType("custom-license-key"); licenseManager.setLicenseKey(type, "CUS-TOM-LIC-KEY"); Object licenseKeyValue = licenseManager.getLicenseKey(type); // выводит CUS-TCM-LIC-KEY System.out.printin(licenseKeyValue); } } Решение, соответствующее принятым нормам (конечный класс) В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, класс LicenseType объявляется как конечный, в следовательно, его методы нельзя переопределить. final class LicenseType { // ... } Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, демонстрируется применение классов Widget и LayoutManager, содержа- щих ряд виджетов. public class Widget { private int noOfComponents; public Widget(int noOfComponents) { this.noOfComponents = noOfComponents; } public int getNoOfComponents() { return noOfComponents; } public final void setNoOfComponents(int noOfComponents) { this.noOfComponents = noOfComponents; } public boolean equals(Object o) { if (o == null || ! (o instanceof Widget) ) { return false; } Widget widget = (Widget) o; return this.noOfComponents == widget.getNoOfComponents(); }
64 Глава 1 Безопасность *@Override public int hashCode() { int res = 31; res = res * 17 + noOfComponents ; return res; } } public class LayoutManager { private Set<Widget> layouts = new HashSet<Widget>(); public void addWidget(Widget widget) { if (!layouts.contains(widget)) { layouts.add(widget); } } public int getLayoutSize() { return layouts.size(); } } Совершающий атаку злоумышленник может расширить класс Widget в качестве виджета типа Navigator и переопределить метод hashCode (), как показано ниже. public class Navigator extends Widget { public Navigator(int noOfComponents) { super(noOfComponents); } @Override public int hashCode() { int res = 31; res = res * 17; return res; } } Ниже приведен исходный код зловредной клиентской программы. Widget nav = new Navigator (1) ; Widget widget = new Widget(1); LayoutManager manager = new LayoutManager () ; manager.addWidget(nav); manager.addWidget(widget); System.out.printin(manager.getLayoutSize()); // вывести 2 Предполагается, что множество layouts содержит лишь один элемент, поскольку коли- чество введенных компонентов как навигатора, так и виджета равно 1. Тем нее менее метод getLayoutSize () возвращает значение 2. Такое расхождение объясняется тем, что метод hashCode () из класса Widget применяется только один раз, когда виджет вводится во мно- жество. Когда же в него вводится навигатор, то применяется метод hashCode () из класса Navigator. Следовательно, во множестве оказываются два разных экземпляра объекта. Решение, соответствующее принятым нормам (конечный класс) В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, класс Widget объявляется как конечный, а следовательно, его методы нельзя переопределить.
15. Не полагайтесь на методы, которые могут быть переопределены... 65 public final class Widget { // ... } Пример кода, не соответствующего принятым нормам (переопределение метода run ()) В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, класс Worker и его подкласс SubWorker содержат метод startThread(), предназначенный для запуска потока на исполнение. public class Worker implements Runnable { Worker () { } public void startThread(String name) { new Thread(this, name).start (); } ©Override public void run() { System.out.printin("Parent"); } } public class SubWorker extends Worker { ©Override public void startThread(String name) { super.startThread(name); new Thread(this, name).start(); } ©Override public void run() { System.out.printin("Child"); } } Если выполнить на стороне клиента следующий фрагмент кода: Worker w = new SubWorker(); w.startThread("thread"); то на стороне клиента можно ожидать вывода на печать слов Parent и Child. Тем не менее слово Child печатается дважды, поскольку переопределяемый метод run () вызывается оба раза, когда начинается новый поток исполнения. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасно- го программирования на Java, видоизменяется класс SubWorker и удаляется вызов метода super.startThread(). public class SubWorker extends Worker { ©Override public void startThread(String name) { new Thread(this, name).start(); } // ... }
66 Глава 1 Безопасность В клиентский код также вносятся изменения, чтобы родительский и порожденный пото- ки начинались раздельно. В частности, приведенный ниже клиентский код дает ожидаемый результат. Worker wl = new Worker () ; wl.startThread("parent-thread"); Worker w2 = new SubWorker(); w2.startThread("child-thread"); Библиография [API 2013] Class Identi tyHashMap [Hawtin 2006] [drlvm][kernel_dasses] ThreadLocal vulnerability 16. Старайтесь не предоставлять излишние полномочия Правила безопасности, принятые в Java, предписывают предоставление коду прав на до- ступ к конкретным системным ресурсам. Источник кода (объект типа CodeSource), которо- му предоставляются полномочия, состоит из местоположения кода (URL) и ссылки на один или несколько сертификатов, содержащих открытые ключи, соответствующие секретным ключам, применяемым для цифровой подписи кода. Ссылка на сертификат уместна лишь в том случае, если код имеет цифровую подпись. Защищенная область охватывает объект типа CodeSource и полномочия, предоставляемые коду из этого объекта в соответствии с дейс- твующими правилами безопасности. Следовательно, классы, подписанные одним и тем же ключом и происходящие из одного и того же места по заданному URL, размещаются в одной защищенной области. Класс может принадлежать одной и только одной защищенной области. А классы с одинаковыми полномочиями, но разными источниками кода принадлежат разным защищенным областям. Каждый класс Java выполняется в соответствующей области, определяемой его источником кода. Любому коду, работающему под управлением диспетчера защиты, должны быть предо- ставлены полномочия на выполнение конкретного безопасного действия (например, чтения или записи в файл). Привилегированный код получает доступ к привилегированным ресурсам от имени непривилегированного кода с помощью метода Accesscontroller. doPrivileged (). Это необходимо, например, в том случае, если системной утилите требуется открыть файл шрифта от имени пользователя, чтобы отобразить документ, но у приложения недостаточно для этого полномочий. Для выполнения такого действия системная утилита использует все свои полномочия, чтобы получить нужные шрифты, игнорируя привилегии того, кто ее вы- зывает. Привилегированный код выполняется со всеми привилегиями защищенной области, связанной с источником кода. Эти привилегии нередко превышают полномочия, необходимые для выполнения привилегированного действия. В идеальном случае коду должны быть предо- ставлены только минимальные привилегии, необходимые для выполнения операции. В реко- мендации 19 “Определяйте специальные полномочия доступа для мелкоструктурной защиты” описывается другой подход к исключению лишних привилегий. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, демонстрируется библиотечный метод, позволяющий выполнять в
16. Старайтесь не предоставлять излишние полномочия 67 вызывающем коде привилегированную операцию (например, чтение из файла), используя метод-оболочку performActionOnFile (). private Fileinputstream openFile() { final Fileinputstream f [] = { null }; Accesscontroller.doPrivileged(new PrivilegedAction() { public Object run() { try { f [0] = new Fileinputstream("file"); } catch(FileNotFoundException fnf) { // направить обработчику ошибок } return null; } }); return f[0]; } // метод-оболочка public void performActionOnFile() { try (Fileinputstream f = openFile()){ // выполнить операцию } catch (Throwable t) { // обработать исключение } } В данном примере безопасному коду предоставляются привилегии, выходящие за рамки чтения из файла, несмотря на то, что кодовому блоку в методе doPrivileged () требовалось лишь право на доступ к файлу только для чтения. Следовательно, этот код нарушает принцип наименьших привилегий, поскольку упомянутому кодовому блоку в нем предоставляются чрезмерные полномочия. Решение, соответствующее принятым нормам В варианте с двумя аргументами метод doPrivileged () принимает в качестве перво- го аргумента объект типа AccessControlContext из вызывающего кода и ограничивает привилегии содержащегося в нем кода теми полномочиями, которые находятся на пересе- чении контекста, передаваемого в качестве второго аргумента, с привилегиями защищенной области. Следовательно, если в вызывающем коде требуется предоставить только право на чтение из файла, то для этого достаточно передать контекст с полномочиями только на чтение из файла. Объект типа AccessControlContext, предоставляющий соответствующие пол- номочия на чтение из файла, может быть создан во внутреннем классе, как показано ниже. Если же в вызывающем коде недостает полномочий для создания соответствующего объекта типа AccessControlContext, то для получения его экземпляра в ней можно вызвать метод Accesscontroller.getContext(). private Fileinputstream openFile(AccessControlContext context) { if (context == null) { throw new SecurityException("Missing AccessControlContext"); }
68 Глава 1 Безопасность final Fileinputstream f [ ] = { null }; Accesscontroller.doPrivileged( new PrivilegedAction() { public Object run() { try { f [0] = new Fileinputstream("file"); } catch (FileNotFoundException fnf) { // направить обработчику ошибок } return null; } }, // ограничить привилегии, передав контекст в качестве аргумента context); return f[0] ; } private static class FileAccessControlContext { public static final AccessControlContext INSTANCE; static { Permission perm = new java.io.FilePermission("file", "read"); Permissioncollection perms = perm.newPermissionCollection(); perms.add(perm); INSTANCE = new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, perms)}); } } // метод-оболочка public void performActionOnFile() { try (final Fileinputstream f = // предоставить права на открытие файла только для чтения openFile(FileAccessControlContext.INSTANCE)){ // выполнить действие } catch (Throwable t) { /1 обработать исключение } } Применимость Если не придерживаться принципа наименьших привилегий, то в конечном итоге это может привести к выполнению в ненадежном, непривилегированном коде непреднамеренно привилегированного действия. Но в то же время тщательное ограничение привилегий услож- няет код. Поэтому приходится идти на компромисс между усложнением кода, а следователь- но, и его сопровождения, и повышением безопасности. Библиография [API 2013] Class Accesscontroller [Oracle 2013a] API for Privileged Blocks
17. Сводите к минимуму объем привилегированного кода 69 17. Сводите к минимуму объем привилегированного кода Для соблюдения в программах принципа наименьших привилегий требуется не только предоставлять привилегированные кодовые блоки с минимальными правами на выполнение надлежащей операции (см. рекомендацию 16 “Старайтесь не предоставлять излишние пол- номочия”), но и обеспечивать наличие в привилегированном коде только тех операций, ко- торые требуют повышения полномочий. Ведь излишний код в привилегированном кодовом блоке должен оперировать привилегиями этого блока, расширяя поле для нарушения безо- пасности. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, применяется метод changePassword (), в котором предпринимается попыт- ка открыть файл с паролем в кодовом блоке метода doPrivileged () и выполнить операции, используя этот файл. В кодовом блоке метода doPrivileged () содержится также излишний вызов метода System. loadLibrary (), загружающего библиотеку аутентификации. public void changePassword(String currentpassword, String newPassword) { final Fileinputstream f [ ] = { null }; Accesscontroller.doPrivileged(new PrivilegedAction() { public Object run() { try { String passwordFile = System.getProperty("user.dir") + File.separator + "PasswordFileName"; f [0] = new Fileinputstream(passwordFile); // проверить, совпадает ли старый пароль с тем, что хранится //в файле, и если он не совпадает, то сгенерировать исключение System.loadLibrary("authentication"); } catch (FileNotFoundException cnf) { // направить обработчику ошибок } return null; } }); // конец метода doPrivileged() } В данном примере кода нарушается принцип наименьших привилегий, поскольку из не- привилегированного вызывающего кода можно также вызвать библиотеку аутентификации. Из непривилегированного вызывающего кода нельзя вызвать метод System. loadLibrary () непосредственно, поскольку это могло бы сделать собственные методы доступными для не- привилегированного кода [SCG 2010]. Кроме того, в методе System. loadLibrary () про- веряются только привилегии того кода, который непосредственно его вызывает, и поэтому пользоваться им следует очень осторожно. Подробнее об этом см. в рекомендации 18 “Не раскрывайте методы с нестрогими проверками ненадежного кода”.
70 Глава 1 Безопасность Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, вызов метода System.loadLibrary () выносится за пределы ко- дового блока метода doPrivileged (). Благодаря этому в непривилегированном коде мож- но выполнять предварительные проверки пароля с его установкой в исходное состояние, используя файл, но не разрешается загружать библиотеку аутентификации. Вызов метода loadLibrary () мог бы произойти и до предварительных проверок пароля с его установкой в исходное состояние, но в данном примере он отложен из соображений производительности. public void changePassword(String currentpassword, String newPassword) { final Fileinputstream f [] = { null }; Accesscontroller.doPrivileged(new PrivilegedAction() { public Object run() { try { String passwordFile = System.getProperty("user.dir") + File.separator + "PasswordFileName"; f [0] = new Fileinputstream(passwordFile); // проверить, совпадает ли старый пароль с тем, что хранится //в файле, и если он не совпадает, то сгенерировать исключение } catch (FileNotFoundException cnf) { // направить обработчику ошибок } return null; } }); // конец метода doPrivileged() System.loadLibrary("authentication"); } Применимость Сведение к минимуму объема привилегированного кода сокращает поле для нарушения безопасности приложения и упрощает задачу ревизии привилегированного кода. Библиография [API 2013] Class Accesscontroller 18. Не раскрывайте методы с нестрогими проверками ненадежного кода В большинстве методов недостает проверок в диспетчере защиты, поскольку в них не пре- доставляется доступ к таким уязвимым частям, как файловая система. А в тех методах, где такие проверки производятся, проверяются полномочия каждого класса и метода в стеке вы- зовов перед выполнением дальнейших действий. Такая модель защиты предоставляет полный доступ к базовой библиотеке Java ограниченному числу программ (например, аплетам). Она также препятствует уязвимому методу действовать от имени злонамеренного метода, скрыва- ющегося за надежными методами в стеке вызовов.
18. Не раскрывайте методы с нестрогими проверками ненадежного кода 71 Но в некоторых методах производятся нестрогие проверки безопасности, в ходе которых проверяются полномочия только вызывающего метода, а не каждого метода в стеке вызовов. Любой код, вызывающий эти методы, должен гарантировать, что они не могут быть вызваны от имени ненадежного кода. Такие методы перечислены в табл. 1.2. Таблица 1.2. Методы, в которых проверяются полномочия только вызывающего метода java.lang.Class.newlnstance java.lang.reflect.Constructor.newlnstance java.lang.reflect.Field.get* java.lang.reflect.Field.set* java.lang.reflect.Method.invoke java.util.concurrent.atomic.AtomicIntegerFieldUpdater.newUpdater java.util.concurrent.atomic.AtomicLongFieldUpdater.newUpdater java. util. concurrent. atomic. AtomicRef erenceFieldUpdater. newUpdater Методы java.lang.reflect.Field.setAccessible() и getAccessible() предпи- сывают виртуальной машине Java отменить языковые проверки доступа. Вместо этого они выполняют стандартные (и более строгие) проверки в диспетчере защиты, а следовательно, менее подвержены уязвимости, рассматриваемой в данной рекомендации. Тем не менее этими методами следует пользоваться очень аккуратно. В остальных методах типа set* и get* для доступа к полям посредством рефлексии выполняются только языковые проверки доступа, а следовательно, они уязвимы. Загрузчики классов Загрузчики классов позволяют динамически расширять приложение на Java во время выполнения, загружая дополнительные классы. Виртуальная машина Java отслеживает за- грузчик, использовавшийся для загрузки каждого класса. Когда загруженный класс впервые ссылается на другой класс, виртуальная машина Java запрашивает, был ли класс, на который делается ссылка, загружен тем же самым загрузчиком, что и ссылающийся класс. Архитектура загрузчика классов в Java управляет взаимодействием кода, загружаемого из разных источни- ков, позволяя использовать разные загрузчики классов. Такое разделение загрузчиков клас- сов является основополагающим для разделения кода, поскольку оно запрещает зловредному коду доступ с целью нарушить надежный код. Методы, отвечающие за загрузку классов, поручают свои функции загрузчикам тех клас- сов, из методов которых они вызываются. Проверки безопасности, связанные с загрузкой классов, выполняются в загрузчиках классов. Следовательно, любой метод, вызывающий один из этих методов загрузки классов, должен гарантировать, что они не смогут действовать от имени ненадежного кода. Такие методы перечислены в табл. 1.3. Таблица 1.3. Методы, использующие загрузчики классов из вызываемых ими методов java.lang.Class.forName java.lang.Package.getPackage java.lang.Package.getrackages java.lang.Runtime.load j ava.lang.Runtime.loadLibrary java. lang. System, load
72 Глава 1 Безопасность Окончание табл. 1.3 java.lang.System.loadLibrary java.sql.DriverManager.getConnection java. sql. DriverManager. getDriver java. sql. DriverManager. getDriver s java.sql.DriverManager.deregisterDriver java. util. ResourceBundle. getBundle Во всех методах, перечисленных в табл. 1.3, за исключением методов loadLibrary () и load (), проверки в диспетчере защиты не выполняются, поскольку они поручают эти провер- ки соответствующим загрузчикам классов. На практике загрузчик класса в надежный код зачас- тую разрешает вызывать эти методы, тогда как загрузчик класса в ненадежный код может и не обладать подобными привилегиями. Но если загрузчик класса в ненадежный код поручает свои функции загрузчику класса в надежный код, то надежный код становится доступным из нена- дежного кода. В отсутствие таких отношений делегирования полномочий загрузчики классов будут обеспечивать разделение пространства имен, а следовательно, ненадежный код не сможет наблюдать члены загружаемого класса или вызывать методы, относящиеся к надежному коду. Модель делегирования полномочий загрузчикам классов является основополагающей для многих реализаций каркасов приложений на Java. В связи с этим рекомендуется не раскры- вать методы, перечисленные в табл. 1.2 и 1.3, ненадежному коду. В качестве примера рассмот- рим нарушение безопасности, когда ненадежный код пытается загрузить привилегированный класс. Если у загрузчика недостаточно полномочий для самостоятельной загрузки запра- шиваемого привилегированного класса, но ему разрешено поручить загрузку этого класса соответствующему загрузчику классов в надежный код, то может произойти превышение полномочий. Более того, если надежный код принимает испорченные вводимые данные, то загрузчик классов в надежный код можно побудить к загрузке привилегированных, зловред- ных классов от имени ненадежного кода. Классы, для которых определен тот же самый загрузчик, будут существовать в одном и том же пространстве имен, но они могут иметь разные привилегии в зависимости от принятых правил безопасности. Уязвимости в защите могут возникнуть в том случае, если привилеги- рованный код сосуществует с непривилегированным (или менее привилегированным) кодом, загруженным тем же самым загрузчиком классов. В данном случае менее привилегированный код может получить свободный доступ к элементам привилегированного кода в соответствии с доступностью, объявленной в привилегированном коде. Если в привилегированном коде применяются любые методы, перечисленные в табл. 1.3, кроме методов loadLibrary () и load (), то он выполняется в обход проверок в диспетчере защиты. Данная рекомендация аналогична рекомендации “SEC03-J. Не загружайте надежные клас- сы после того, как ненадежному коду будет разрешено загружать произвольные классы” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]. Во многих случаях не соблюдается также рекомендация “SECOO-J. Не допускайте утечку секретной информации из блоков привилегированного кода через границу доверия” из того же самого стандарта. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, вызов метода System. loadLibrary () встраивается в кодовый блок метода doPrivileged().
18. Не раскрывайте методы с нестрогими проверками ненадежного кода 73 public void load(String libName) { Accesscontroller.doPrivileged(new PrivilegedAction() { public Object run() { System.loadLibrary(libName); return null; } }); } Данный пример кода оказывается ненадежным потому, что он может быть использован для загрузки библиотеки от имени ненадежного кода. По существу, загрузчик классов в нена- дежный код может воспользоваться данным примером кода для загрузки библиотеки, несмот- ря на то, что у него нет полномочий сделать это непосредственно. После загрузки библиотеки из ненадежного кода могут быть вызваны собственные методы этой библиотеки, если они доступны, поскольку в кодовом блоке метода doPrivileged () не допускается производить любые проверки в диспетчере защиты по отношению к вызывающему коду, находящемуся вверх по стеку исполнения. Несобственный код библиотеки может быть также подвержен аналогичным нарушениям безопасности. Допустим, имеется библиотека с уязвимостью, не раскрываемой непосредс- твенно, но, возможно, находящейся в неиспользуемом методе. В результате загрузки библио- теки такая уязвимость, возможно, и не раскроется. Но совершающий атаку злоумышленник может загрузить дополнительную библиотеку, в которой будет использована эта уязвимость из первой библиотеки. Более того, в библиотеках несобственного кода нередко используются кодовые блоки из метода doPrivileged(), что делает их привлекательными мишенями для нарушения безопасности. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, имя библиотеки жестко кодируется, чтобы исключить всякую воз- можность внедрить испорченные значения. Здесь также сокращается доступность метода load (), который из открытого становится закрытым. Следовательно, библиотеку awt запре- щается загружать из ненадежного вызывающего кода. private void load() { Accesscontroller.doPrivileged(new PrivilegedAction() { public Object run() { System.loadLibrary("awt"); return null; } }); } Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, экземпляр объекта java. sql.Connection возвращается из надежного кода в ненадежный код. public Connection getConnection(String url, String username, String password) { // ... return DriverManager.getConnection(url, username, password);
74 Глава 1 Безопасность В ненадежном коде, которому недостает полномочий для установления соединения с базой данных по запросу SQL, можно обойти эти ограничения, используя непосредственно приобретенный экземпляр упомянутого выше объекта. Метод getConnection () ненадежен, поскольку в нем используется аргумент url для обозначения загружаемого класса, который служит в качестве драйвера базы данных. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, злонамеренным пользователям не разрешается предоставлять свои URL для соединения с базой данных. Благодаря этому ограничиваются их возможности загру- жать ненадежные драйверы базы данных. private String url = // жестко закодированное значение public Connection getConnection(String username, String password) { // ... return DriverManager.getConnection(this.url, username, password); } Пример кода, не соответствующего принятым нормам (уязвимость CERT № 636312) В документе CERT Vulnerability Note VU № 636312 описывается уязвимость, которая была обнаружена в обновлении 6 версии Java 1.7.0 и которой злоумышленники широко воспользо- вались для компьютерного вторжения в августе 2012 года. В этом компьютерном вторжении, по существу, были использованы две уязвимости: первая из них рассматривается в данной рекомендации, а вторая — в рекомендации “SEC05-J. Не пользуйтесь рефлексией для повы- шения доступности классов, методов или полей” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012]. Данное компьютерное вторжение выполняется в виде аплета на Java. Загрузчик классов аплета гарантирует, что аплет не сможет непосредственно вызывать методы из классов, вхо- дящих в пакет com. sun. *. А обычная проверка в диспетчере безопасности гарантирует, что конкретные действия разрешаются или запрещаются в зависимости от привилегий всех ме- тодов вызывающего кода в стеке вызовов (эти привилегии связаны с источником кода, охва- тывающим класс). Первой целью данного компьютерного вторжения было стремление получить до- ступ к закрытому классу sun. awt. SunToolkit. Но непосредственный вызов метода class. forName () по имени данного класса приводил к генерированию исключения типа SecurityException. Следовательно, в коде данного компьютерного вторжения применялся приведенный ниже метод доступа к любому классу в обход проверки в диспетчере защиты. private Class GetClass(String paramString) throws Throwable { Object arrayOfObject[] = new Object[1]; arrayOfObject[0] = paramString; Expression localExpression = new Expression(Class.class, "forName”, arrayOfObject); localExpression.execute(); return (Class)localExpression.getValue(); }
18. Не раскрывайте методы с нестрогими проверками ненадежного кода 75 Метод java.beans .Expression.execute () поручает свои функции приведенному ниже методу. private Object invokelnternal () throws Exception { Object target = getTarget(); String methodName = getMethodName () ; if (target == null | I methodName == null) { throw new NullPointerException( (target == null ? "target" : "methodName") + " should not be null"); } Object[] arguments = getArguments (); if (arguments == null) { arguments = emptyArray; } // метод Class. forName О не загрузит классы вне ядра из класса // внутри ядра, и поэтому он интерпретируется как особый случай if (target == Class.class && methodName.equals("forName")) { return ClassFinder.resolveClass( (String)arguments[0], this.loader); } // ... Метод com. sun. beans, finder. ClassFinder. resolveClass () поручает свои функ- ции приведенному ниже методу f indClass (). public static Class<?> findClass(String name) throws ClassNotFoundException { try { ClassLoader loader = Thread.currentThread().getContextClassLoader(); if (loader == null) { loader = ClassLoader.getSystemClassLoader(); } if (loader != null) { return Class.forName(name, false, loader); } } catch (ClassNotFoundException exception) { // использовать вместо этого загрузчик текущего класса } catch (SecurityException exception) { // использовать вместо этого загрузчик текущего класса } return Class.forName(name); } Несмотря на то что данный метод вызывается в контексте аплета, для получе- ния требуемого класса в нем применяется метод Class. forName (), который поруча- ет свои функции загрузчику класса этого метода. В данном случае вызывающий класс (com. sun.beans, finder. ClassFinder) входит в состав ядра Java, и поэтому загрузчик надежных классов применяется вместо загрузчика классов аплета с более ограниченными полномочиями. И в конечном итоге загрузчик надежных классов загружает требуемый класс, даже не подозревая о том, что он действует от имени зловредного кода.
76 Глава 1 Безопасность Решение, соответствующее принятым нормам (CVE-2012-4681) В обновлении 7 версии Java 1.7.0 компания Oracle устранила данную уязвимость, внеся коррективы в исходный код метода com.sun.beans.finder.ClassFinder.findClass (). В методе checkPackageAccess () проверяется весь стек вызовов, чтобы гарантировать (только в этом случае) извлечение классов исключительно от имени надежных методов. public static Class<?> findClass(String name) throws ClassNotFoundException { checkPackageAccess(name); try { ClassLoader loader = Thread.currentThread().getContextClassLoader (); if (loader == null) { // может быть пустым значением в Internet Explorer (см. 6204697) loader = ClassLoader.getSystemClassLoader(); } if (loader != null) { return Class.forName(name, false, loader); } } catch (ClassNotFoundException exception) { // использовать вместо этого загрузчик текущего класса } catch (SecurityException exception) { // использовать вместо этого загрузчик текущего класса } return Class.forName(name); } Пример кода, не соответствующего принятым нормам (CVE-2013-0422) Уязвимости, обнаруженные в обновлении 10 версии Java 1.7.0, были широко исполь- зованы для компьютерного вторжения в январе 2013 года. Одна из таких уязвимостей была обнаружена в классе com. sun. jmx.mbeanserver .MBeanlnstantiator, предо- ставлявшем непривилегированному коду возможность для доступа к любому классу не- зависимо от принятых в данный момент правил безопасности или доступности. В час- тности, метод MBeanlnstantiator. findClass () можно было вызывать с любой символьной строкой, в результате чего предпринималась попытка возвратить объект типа Class, именуемый по данной символьной строке. Этот метод поручал свои функции методу MBeanlnstantiator.loadClass (), исходный код которого приведен ниже. static Class<?> loadClass(String className, ClassLoader loader) throws ReflectionException { Class<?> theClass; if (className == null) { throw new RuntimeOperationsException( new IllegalArgumentException( "The class name cannot be null"), "Exception occurred during object instantiation"); } try { if (loader == null) { loader = MBeanlnstantiator.class.getClassLoader(); } if (loader != null) {
18. Не раскрывайте методы с нестрогими проверками ненадежного кода 77 theClass = Class.forName(className, false, loader); } else { theClass = Class.forName(className); } } catch (ClassNotFoundException e) { throw new ReflectionException( e, "The MBean class could not be loaded"); } return theClass; } Этот метод поручает задачу динамической загрузки указанного класса мето- ду Class. forName (), который, в свою очередь, поручает решить эту задачу загруз- чику класса вызывающего метода. А поскольку вызывающим оказывается метод MBeanlnstantiator. loadClass (), то используется загрузчик базового класса, не обеспе- чивающий никаких проверок безопасности. Решение, соответствующее принятым нормам (CVE-2013-0422) В обновлении И версии Java 1.7.0 компания Oracle устранила данную уязвимость, вве- дя проверку прав доступа в исходный код метода MBeanlnstantiator. loadclass (). Эта проверка гарантирует, что вызывающему коду разрешается доступ к искомому классу, как показано ниже. // ... if (className == null) { throw new RuntimeOperationsException( new IllegalArgumentException( "The class name cannot be null"), "Exception occurred during object instantiation"); } ReflectUtil.checkPackageAccess(className); try { if (loader == null) // ... Применимость Если разрешить в ненадежном коде вызов методов с нестрогими проверками безопаснос- ти, то в конечном итоге это может привести в превышению полномочий. А если разрешить в ненадежном коде выполнение действий с помощью загрузчика классов в непосредственно вызывающем коде, то ненадежный код может получить разрешение на выполнение с теми же самыми привилегиями, что и в непосредственно вызывающем коде. Рассмотрение методов, в которых экземпляр загрузчика классов в непосредственно вы- зывающем коде не применяется, выходит за рамки данной рекомендации. Например, в ва- рианте метода java. lang.Class. forName () с тремя аргументами требуется явный аргу- мент, обозначающий экземпляр используемого загрузчика классов, как показано ниже. Если экземпляры объектов должны быть возвращены ненадежному коду, то ни в коем случае не указывайте в качестве третьего аргумента данного метода загрузчик классов непосредственно в вызывающем коде. public static Class forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
78 Глава 1 Безопасность Библиография [API 2013] [Chan 1998] [Guillardoy 2012] [Long 2012] [Manion 2013] [Oracle 2013d] Class ClassLoader java.lang.reflect AccessibleObject Java ODay Analysis (CVE-2012-4681) SECOO-J. Do not allow privileged blocks to leak sensitive information across a trust boundary SEC03-J. Do not load trusted classes after allowing untrusted code to load arbitrary classes SEC05-J. Do not use reflection to increase accessibility of classes, methods, or fields "Anatomy of Java Exploits" Oracle Security Alert for CVE-2013-0422 19. Определяйте специальные полномочия доступа для мелкоструктурной защиты По умолчанию в диспетчере защиты проверяется, имеется ли у кода, вызывающего метод, достаточно полномочий для выполнения конкретного действия. Это действие определяется в архитектуре безопасности языка Java как уровень доступа и требует определенных пол- номочий для своего выполнения. Например, полномочия java. io. FilePermission пре- доставляются для выполнения следующих действий: чтение, запись, выполнение и удаление [API 2013]. В рекомендации “Описание полномочий и риски” [Oracle 201 Id] перечисляются доступные по умолчанию полномочия и риски, связанные с их предоставлением прикладному коду на Java. Но иногда требуются более строгие ограничения, чем те, что накладываются диспетчером защиты по умолчанию. Если не предоставить специальные полномочия в отсутствие соот- ветствующих полномочий по умолчанию, то возможно превышение полномочий, позволя- ющее ненадежному вызывающему коду выполнять операции или действия ограниченного применения. В представленной здесь рекомендации рассматриваются вопросы превышения полномочий и способы их разрешения. А другие способы разрешения подобных затруднений в обеспечении безопасности рассматриваются в рекомендации 16 “Старайтесь не предостав- лять излишние полномочия”. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, содержится привилегированный кодовый блок, используемый для выполне- ния двух уязвимых операций: загрузки библиотеки и установки обработчика исключений по умолчанию. class LoadLibrary { private void loadLibrary() { Accesscontroller.doPrivileged( new PrivilegedAction() { public Object run() { // привилегированный код System.loadLibrary("myLib.so"); // выполнить уязвимую операцию вроде установки // обработчика исключений по умолчанию MyExceptionReporter.setExceptionReporter(reporter);
19. Определяйте специальные полномочия доступа для мелкоструктурной защиты 79 return null; } }); } } Когда используется диспетчер защиты по умолчанию, он запрещает загрузку библиотеки, если только в файле правил защиты не предоставляются полномочия loadLibrary .myLib типа RuntimePermission. Но диспетчер защиты не защищает вызывающий код автома- тически от выполнения другой уязвимой операции установки обработчика исключений по умолчанию, поскольку полномочия на выполнение этой операции не предоставляются по умолчанию, а следовательно, они недоступны. Подобной уязвимостью в системе безопаснос- ти можно, например, воспользоваться с целью создать и установить обработчик исключений, обнаруживающий информацию, которую отсеял бы допустимый обработчик исключений. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам безопасного программирования на Java, определяются специальные полномочия exc.reporter типа ExceptionReporterPermission, запрещающие недопустимому вызывающему коду ус- танавливать обработчик исключений по умолчанию. С этой целью выполняется подклас- сификация класса Basic Permiss ion, позволяющего устанавливать полномочия в стиле двоичной логики, т.е. разрешать или запрещать конкретные действия. Далее в рассматри- ваемом здесь решении используется диспетчер защиты, чтобы проверить, уполномочен ли вызывающий код устанавливать обработчик исключений. Если эта проверка не проходит, то генерируется исключение типа SecurityException. В специальном классе полномочий ExceptionReporterPermission определяются также два требующихся конструктора. class LoadLibrary { private void loadLibrary() { Accesscontroller.doPrivileged( new PrivilegedAction() { public Object run() { // привилегированный код System.loadLibrary("myLib.so"); // выполнить уязвимую операцию вроде установки // обработчика исключений по умолчанию MyExceptionReporter.setExceptionReporter(reporter); return null; } }); } } final class MyExceptionReporter extends ExceptionReporter { public void setExceptionReporter(ExceptionReporter reporter) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkpermission( new ExceptionReporterPermission("exc.reporter")); } // продолжить, чтобы установить уведомитель об исключении }
80 Глава 1 Безопасность // ... другие методы класса MyExceptionReporter } final class ExceptionReporterPermission extends BasicPermission { public ExceptionReporterPermission(String permName) { super(permName); } // этот конструктор должен быть определен, даже если // параметр actions игнорируется public ExceptionReporterPermission(String permName, String actions) { super(permName, actions); } В файле правил защиты должны быть предоставлены следующие полномочия: exc.reporter типа ExceptionReporterPermission и loadlibrary.myLib типа RuntimePermission. В приведенном ниже файле правил защиты предполагается, что преды- дущие источники находятся в каталоге с: \package операционной системы вроде Windows. grant CodeBase "file:/с:/package" { // для UNIX-подобных систем file:${user.home}/package/ permission ExceptionReporterPermission "exc.reporter"; permission java.lang.RuntimePermission "loadLibrary.myLib"; }; По умолчанию полномочия не могут быть определены для поддержки действий с по- мощью класса BasicPermission, но если требуется, то действия могут быть свободно ре- ализованы в подклассе, производном от класса ExceptionReporterPermission. Класс BasicPermission является абстрактным, хотя и не содержит абстрактные методы. В нем определяются все методы, расширяемые из класса Permission. В специально определяемом подклассе, производном от класса BasicPermission, должны быть определены два конс- труктора для вызова наиболее подходящего конструктора суперкласса (с одним или двумя аргументами), поскольку конструктор по умолчанию в суперклассе отсутствует. Кроме того, конструктор с двумя аргументами принимает действие, даже если ему не требуются элемен- тарные полномочия. Такое поведение требуется для построения объектов полномочий из файла правил защиты. Следует, однако, иметь в виду, что подкласс, производный от класса BasicPermission, объявляется как конечный (final). Применимость Если выполнить код Java, не определив специальные полномочия там, где устанавливае- мые по умолчанию полномочия неприемлемы, то приложение может оказаться уязвимым в отношении превышения полномочий. Библиография [API 2013] [Oaks 2001] [Oracle 2011d] [Oracle 2013c] [Policy 2010] Class FilePermission Class SecurityManager "Permissions" subsection of Chapter 5, "The Access Controller," Permissions in the Java™ SE 6 Development Kit (JDK) Java Platform Standard Edition 7 Documentation "Permission Descriptions and Risks"
20. Создавайте безопасную "песочницу" используя диспетчер защиты 81 20. Создавайте безопасную "песочницу" используя диспетчер защиты В документации на класс SecurityManager из прикладного интерфейса Java API [API 2013] говорится следующее: “Диспетчер защиты — это класс, позволяющий реализовать в приложениях правила за- щиты. С его помощью в приложении можно определить характер операции и возмож- ность ее выполнения в безопасном контексте, где разрешается это делать, прежде чем пытаться выполнять, возможно, ненадежную или уязвимую операцию. Таким образом, в приложении можно разрешить или запретить выполнение конкретной операции”. Диспетчер защиты может быть связан с любым кодом Java. Так, диспетчер защиты аплетов отклоняет все привилегии аплетов, кроме самых существенных. Он предназначен для защиты от непреднамеренного видоизменения системы, утечки информации и обезличивания поль- зователей. Применение диспетчеров защиты не ограничивается только защитой приложений на стороне клиента. На таких веб-серверах, как Tomcat и WebSphere, это средство применя- ется для изолирования троянских сервлетов и зловредных серверных страниц на Java (JSP), а также для защиты уязвимых системных ресурсов от непреднамеренного доступа. В приложениях на Java, выполняющихся из командной строки, используемый по умолча- нию или специальный диспетчер защиты может быть установлен с помощью опции команд- ной строки. С другой стороны, диспетчер защиты можно установить программно. В послед- нем случае имеется также возможность создать используемую по умолчанию “песочницу”, которая разрешает или запрещает выполнение уязвимых действий в зависимости от действу- ющих правил безопасности. Начиная с версии Java 2 SE Platform класс SecurityManager стал неабстрактным. В ито- ге отпала явная потребность в переопределении его методов. Для программной установки и применения диспетчера защиты прикладному коду должны быть предоставлены на время выполнения полномочия типа createSecurityManager (для получения экземпляра класса SecurityManager) и setSecurityManager (для его установки). Эти полномочия проверя- ются только в том случае, если диспетчер защиты уже установлен. Это удобно, если диспетчер защиты установлен, например, на виртуальном хосте, а на отдельных хостах должны быть от- клонены полномочия, требующиеся для переопределения диспетчера защиты по умолчанию специальным диспетчером. Диспетчер защиты тесно связан с классом Accesscontroller. Первый из них служит в качестве ядра для управления доступом, тогда как последний обеспечивает конкретную реа- лизацию алгоритма управления доступом. Диспетчер защиты поддерживает следующие воз- можности. Обеспечение обратной совместимости. Устаревший код нередко содержит специ- альные реализации класса диспетчера защиты, поскольку изначально он был абс- трактным. Определение специальных правил защиты. Подклассификация класса диспетчера защиты допускает определение специальных правил защиты (например, многоуровне- вой, предварительной или мелкоструктурной).
82 Глава 1 Безопасность Что касается реализации и применения специальных диспетчеров защиты, в отличие от используемых по умолчанию, то в спецификации на архитектуру системы безопасности языка Java [SecuritySpec 2010] по этому поводу говорится следующее: “Применение класса Accesscontroller поощряется в прикладном коде, тогда как специальная настройка диспетчера защиты (путем подклассификации) должна быть последним средством, к которому следует прибегать лишь в самом крайнем случае. Более того, в специально настроенном диспетчере защиты, например, всегда прове- ряющем время суток перед выполнением стандартных проверок безопасности, может и должен применяться везде, где это уместно, алгоритм, предоставляемый классом Accesscontroller” Во многих прикладных интерфейсах Java SE API проверки в диспетчере защиты произ- водятся по умолчанию перед выполнением уязвимых операций. Например, конструктор класса java. io. Fileinputstream генерирует исключение типа SecurityException, если у вызывающего кода нет полномочий на чтение из файла. А поскольку подкласс SecurityException является производным от класса RuntimeException, то в объявле- ниях некоторых методов из прикладного интерфейса API (например, методов класса j ava .io.FileReader) может недоставать операторов throws, в которых перечисляются исклю- чения типа SecurityException. В связи с этим следует избегать зависимости от наличия или отсутствия тех проверок в диспетчере защиты, которые не указаны в документации на методы из прикладного интерфейса API. Пример кода, не соответствующего принятым нормам (установка диспетчера защиты из командной строки) В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, не удается установить ни один из диспетчеров защиты из командной строки. Следовательно, программа будет выполняться со всеми разрешенными полномочиями. Это означает, что ни один из диспетчеров защиты не воспрепятствует возможному выполнению любых недобросовестных действий в программе. java LocalJavaApp Решение, соответствующее принятым нормам (файл правил защиты по умолчанию) В любой программе на Java можно попытаться установить диспетчер защиты типа SecurityManager программным способом, хотя активный в настоящий момент диспетчер защиты может запретить эту операцию. В приложениях, предназначенных для локального выполнения, можно указать используемый по умолчанию диспетчер защиты с помощью при- знака при запуске на выполнение из командной строки. Установка диспетчера защиты из командной строки более предпочтительна, когда прило- жениям должно быть запрещено устанавливать специальные диспетчеры защиты програм- мно и требуется, чтобы они в любом случае соблюдали устанавливаемые по умолчанию пра- вила безопасности. В представленном здесь решении, соответствующем принятым нормам безопасного программирования на Java, диспетчер защиты по умолчанию устанавливается с помощью соответствующих опций командной строки. А файл правил защиты предоставляет приложению полномочия для выполнения предполагаемых в нем действий. java -Djava.security.manager -Djava.security.policy=policyURL LocalJavaApp
20. Создавайте безопасную "песочницу" используя диспетчер защиты 83 В опциях командной строки можно указать специальный диспетчер защиты, правила ко- торого соблюдаются глобально. Для этого опция -D java. security .manager указывается следующим образом: java -Djava. security. manager=my. security. CustomManager . . . Если текущие правила безопасности, соблюдаемые текущим диспетчером защиты, запре- щают любые замены, опуская директиву RuntimePermission ("setSecurityManager") в файле правил защиты, то любая попытка вызвать метод setSecurityManager () приве- дет к появлению исключения типа SecurityException. Используемый по умолчанию файл правил защиты java.policy находится в каталоге /path/to/java.home/lib/security в UNIX-подобных системах, а его аналог в системах типа Windows предоставляет лишь не- которые полномочия, в том числе на чтение системных свойств, привязку к непривилегиро- ванным портам и т.д. Файл правил защиты для каждого пользователя в отдельности может располагаться в начальном каталоге пользователя. Совместно эти файлы правил защиты оп- ределяют полномочия, предоставляемые программе. В файле java. security можно указать, какие именно файлы правил защиты следует использовать. Если же удалить любой из файлов java.policy или java.security на системном уровне, то выполняющейся программе на Java не будет предоставлено никаких полномочий. Решение, соответствующее принятым нормам (специальный файл правил защиты) Заменяя глобальный для Java файл правил защиты на специальный, пользуйтесь двойным (=), а не одинарным (=) знаком равенства, как показано ниже. java -Djava.security.manager -Djava.security.policy==policyURL LocalJavaApp Решение, соответствующее принятым нормам (дополнительные файлы правил защиты) Утилита appletviewer автоматически устанавливает диспетчер защиты со стандартным файлом правил защиты. Для того чтобы указать дополнительные файлы правил защиты, вос- пользуйтесь опцией командной строки - J следующим образом: appletviewer -J-Djava.security.manager \ - J-D java. security. policy=policyURL LocalJavaApp Следует, однако, иметь в виду, что файл правил защиты, указываемый в качестве аргу- мента командной строки, игнорируется, когда в свойстве policy. allowSystemProperty из файла свойств защиты (java.security) устанавливается логическое значение false; а по умолчанию в нем устанавливается логическое значение true. Вопросы составления файлов правил защиты и соответствующий синтаксис подробно обсуждаются в документе “Default Policy Implementation and Policy File Syntax” (Реализация правил защиты по умолчанию и син- таксис файла этих правил) [Policy 2010]. Пример кода, не соответствующего принятым нормам (установка диспетчера защиты программно) Диспетчеры защиты типа SecurityManager могут быть также активизированы с по- мощью статического метода System.setSecurityManager (), но по очереди. Этот метод
84 Глава 1 Безопасность заменяет активный в настоящий момент диспетчер защиты тем диспетчером, который указан в качестве его аргумента, или не делает этого, если его аргумент имеет пустое значение null. В данном примере кода, не соответствующего принятым нормам безопасного про- граммирования на Java, дезактивизируется любой текущий диспетчер защиты типа SecurityManager, но вместо него не устанавливается другой диспетчер. Следовательно, последующий код будет выполняться со всеми разрешенными полномочиями и без всяких ограничений на выполнение возможных недобросовестных действий. Активный диспетчер защиты типа SecurityManager, соблюдающий благоразумные правила защиты, будет пре- пятствовать системе дезактивировать его, в результате чего в данном коде возникает исклю- чение типа SecurityException. try { System.setSecurityManager(null); } catch (SecurityException se) { // невозможно установить диспетчер защиты, сделать / / соответствующую запись в файле регистрации ошибок } Решение, соответствующее принятым нормам (установка диспетчера защиты по умолчанию) В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, получается экземпляр класса и устанавливается используемый по умолчанию диспетчер защиты. try { System.setSecurityManager(new SecurityManager()); } catch (SecurityException se) { // невозможно установить диспетчер защиты, сделать // соответствующую запись в файле регистрации ошибок } Решение, соответствующее принятым нормам (установка специального диспетчера защиты) В представленном здесь решении, соответствующем принятым нормам безопасно- го программирования на Java, показывается, каким образом получается экземпляр класса CustomSecurityManager специального диспетчера защиты. С этой целью вызывается его конструктор с паролем. После этого специальный диспетчер защиты устанавливается в качес- тве активного диспетчера. char password[] = /* инициализировать */ try { System.setSecurityManager(new CustomSecurityManager("password here")) ; } catch (SecurityException se) { // невозможно установить диспетчер защиты, сделать // соответствующую запись в файле регистрации ошибок } После выполнения этого кода специальный диспетчер защиты будет использоваться в при- кладных интерфейсах API, выполняющих проверки безопасности. Как отмечалось ранее, спе- циальные диспетчеры защиты должны быть установлены только в том случае, если используе- мому по умолчанию диспетчеру защиты недостает требуемых функциональных возможностей.
21. Не допускайте злоупотреблений привилегиями методов обратного вызова... 85 Применимость Безопасность в Java, по существу, зависит от существования диспетчера защиты. В его отсутствие уязвимые действия могут выполняться без всяких ограничений. Обнаружить программным способом наличие или отсутствие диспетчера защиты во время выполнения нетрудно. Статический анализ позволяет выявить наличие или отсутствие кода, который по- пытается установить диспетчер защиты, если он будет выполнен. В некоторых особых слу- чаях можно проверить, установлен ли диспетчер защиты заранее, заданы ли требуемые его свойства и гарантируется ли его установка в дальнейшем, но, как правило, эта задача нераз- решима. Вызов метода setSecurityManager () может быть опущен в тех контролируемых средах, где заранее известно, что глобальный диспетчер защиты по умолчанию всегда устанавливает- ся из командной строки. Но соблюсти этот правило трудно, а если среда сконфигурирована неверно, то в ней могут появиться уязвимости. Библиография [API 2013] Class SecurityManager Class AccessControlContext Class Accesscontroller [Gong 2003] §6.1, "Security Manager" [Pistoia 2004] §7.4, "The Security Manager" [Policy 2010] Default Policy Implementation and Policy File Syntax [SecuritySpec 2010] §6.2, "SecurityManager versus AccessController" 21. He допускайте злоупотреблений привилегиями методов обратного вызова в ненадежном коде Обратные вызовы предоставляют средства для регистрации метода, обратно вызываемого при наступлении интересного события. В Java обратные вызовы применяются для обработки событий в течение срока действия аплетов и сервлетов, уведомления о наступлении таких событий в компонентах AWT и Swing, как выбор кнопок щелчком кнопкой мыши, асинх- ронного чтения и записи данных в запоминающем устройстве и даже для автоматического выполнения указанного метода run () в новом потоке, запускаемом на выполнение в методе Runnable. run (). Как правило, обратные вызовы реализуются в Java с помощью интерфей- сов. Ниже приведена общая структура обратного вызова. public interface CallBack { void callMethodO ; } class CallBacklmpl implements CallBack { public void callMethodO { System.out.printin("CallBack invoked"); } } class CallBackAction {
86 Глава 1 Безопасность private CallBack callback; public CallBackAction(CallBack callback) { this.callback = callback; } public void perform() { callback.caliMethodf); } } class Client { public static void main(String[] args) { CallBackAction action = new CallBackAction(new CallBacklmpl()); // ... action.perform(); // вывести сообщение "CallBack invoked" // (Обратный вызов сделан) } } Методы обратного вызова нередко вызываются без изменений в привилегиях, а это озна- чает, что они могут быть выполнены в контексте, обладающем большими привилегиями, чем контекст, в котором они объявлены. Если методы обратного вызова принимают данные из не- надежного кода, это может привести к превышению полномочий. В рекомендациях компании Oracle по безопасному программированию на Java [SCG 2010] говорится следующее: “Методы обратного вызова, как правило, вызываются из системы с полными полно- мочиями. Было бы вполне благоразумно предположить, что для выполнения опера- ции в стеке вызовов должен присутствовать злонамеренный код, но это совсем не так. В злонамеренном коде могут быть установлены объекты, соединяющие обратный вы- зов с операцией, проверяемой на безопасность. Например, в диалоговом окне выбора файлов, где производятся действия пользователя по манипулированию файловой сис- темой, могут наступить события, инициируемые в злонамеренном коде. С другой сто- роны, под благопристойной личиной диалогового окна выбора файлов злонамеренный код может переадресовать события, наступающие в пользовательском интерфейсе”. Эта рекомендация является частным случаем рекомендации 17 “Сводите к минимуму объем привилегированного кода” и связана с рекомендацией “SEC01-J. Не допускайте появ- ления испорченных переменных в блоках привилегированного кода” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]). Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, используется класс UserLookupCallBack, реализующий интерфейс CallBack для поиска имени пользователя по заданному его идентификатору. В приведенном ниже коде поиска предполагается, что эти сведения находятся в файле /etc/passwd, для от- крытия которого требуется повысить полномочия. Следовательно, все вызовы в классе Client делаются с повышенными полномочиями (в кодовом блоке метода doPrivileged ()). public interface CallBack { void callMethodf) ; } class UserLookupCallBack implements CallBack {
21. Не допускайте злоупотреблений привилегиями методов обратного вызова... 87 private int uid; private String name; public UserLookupCallBack(int uid) { this, uid = uid; } public String getNameO { return name; } public void callMethodO { try (Inputstream fis = new Fileinputstream("/etc/passwd")) { // найти идентификатор и присвоить его имени пользователя } catch (lOException х) { name = null; } } } final class CallBackAction { private CallBack callback; public CallBackAction(CallBack callback) { this.callback = callback; } public void perform () { AccessController.doPrivileged (new PrivilegedAction<Void>() { public Void run () { callback.callMethodO ; return null; } }); } } Этот код может быть благополучно использован на стороне клиента следующим образом: public static void main(String[] args) { int uid = Integer.parselnt(args[ 0 ]); CallBack callBack = new UserLookupCallBack(uid); CallBackAction action = new CallBackAction(callBack); // ... action.perform(); // найти имя пользователя System.out.printin("User " + uid + " is named " + callBack.getName()); } Но совершающий атаку злоумышленник может воспользоваться классом CallBackAction для выполнения зловредного кода с превышенными полномочиями, зарегистрировав экземп- ляр класса MaliciousCallBack, как показано ниже. class MaliciousCallBack implements CallBack { public void callMethodO { // здесь следует код, выполняемый с превышенными полномочиями
88 Глава 1 Безопасность // клиентский код public static void main(String[] args) { CallBack callback = new MaliciousCallBack(); CallBackAction action = new CallBackAction(callback); action.perform(); // выполнить зловредный код Решение, соответствующее принятым нормам (локальный обратный вызов метода doPrivileged ()) В рекомендациях компании Oracle по безопасному программированию на Java [SCG 2010] говорится следующее: “По принятому соглашению экземпляры классов PrivilegedAction и Privileged ExceptionAction могут быть сделаны доступными для ненадежного кода, но метод doPrivileged () не должен вызываться с действиями, предоставляемыми вызываю- щим кодом” В представленном здесь решении, соответствующем принятым нормам безопасно- го программирования на Java, вызов метода doPrivileged () выносится из кода класса CallBackAction непосредственно в обратный вызов. public interface CallBack { void callMethodO ; class UserLookupCallBack implements CallBack { private int uid; private String name; public UserLookupCallBack(int uid) { this, uid = uid; } public String getName () { return name; } public final void callMethodO { Accesscontroller.doPrivileged(new PrivilegedAction<Void>() { public Void run() { try (Inputstream fis = new Fileinputstream("/etc/passwd")) { // найти идентификатор пользователя и присвоить его // имени UserLookupCallBack. this. name } catch (lOException x) { UserLookupCallBack.this.name = null; } return null; } }); }
21. Не допускайте злоупотреблений привилегиями методов обратного вызова... 89 final class CallBackAction { private CallBack callback; public CallBackAction(CallBack callback) { this.callback = callback; } public void perform() { callback.callMethod(); } } Этот код действует, как и прежде, но теперь совершающему атаку злоумышленнику не удастся выполнить злонамеренный код обратного вызова с превышенными полномочиями. И хотя злоумышленник сможет передать экземпляр зловредного обратного вызова, используя конструктор класса CallBackAction, его код не будет выполнен с превышенными полномо- чиями, поскольку этот экземпляр должен содержать кодовый блок метода doPrivileged (), который не может обладать такими же привилегиями, как и у ненадежного кода. Кроме того, из класса CallBackAction нельзя получить подкласс, чтобы переопределить метод perform (), поскольку этот класс объявлен как конечный. Решение, соответствующее принятым нормам (объявление обратного вызова конечным) В представленном здесь решении, соответствующем принятым нормам безопасного про- граммирования на Java, класс UserLookupCallBack объявляется как конечный, чтобы пре- дотвратить переопределение метода callMethod (). final class UserLookupCallBack implements CallBack { // ... } // остальная часть кода остается без изменения Применимость Если раскрыть уязвимые методы через обратные вызовы, то в конечном итоге это может привести к злоупотреблению привилегиями и выполнению произвольного кода. Библиография [API 2013] Accesscontroller. doPrivileged () [Long 2012] SEC01-J. Do not allow tainted variables in privileged blocks [SCG 2010] Guideline 9-2: Beware of callback methods Guideline 9-3: Safely invoke java.security.AccessController.doPrivileged
Глава Защитное программирование Защитным называется такое программирование, которое тщательно защищено и помогает разрабатывать надежное программное обеспечение таким образом, чтобы максимально обе- зопасить каждый его компонент, например, проверкой достоверности недокументированных допущений [Goodliffe 2007]. Рекомендации, приведенные в этой главе, относятся к тем об- ластям программирования на языке Java, которые помогают ограничить последствия ошибки или благополучно выйти из состояния ошибки. Механизмы, имеющиеся в языке Java, должны применяться для ограничения области и срока действия, а также доступности программных ресурсов. Кроме того, поддерживаемые в Java аннотации могут быть использованы для документирования программы, повышения удобочитаемости ее исходного кода и упрощения ее сопровождения. Программирующие на Java должны принимать во внимание неявные виды поведения и избегать произвольных до- пущений по поводу поведения системы. Общим полезным принципом защитного программирования является простота. Сложные системы, прежде всего, трудно понять, сопровождать и добиться правильного их функциони- рования. Если программная конструкция становится сложной для реализации, следует рас- смотреть возможность ее переделки и реорганизации с целью ее упрощения. И наконец, программа должна быть разработана как можно более надежной. Везде, где только возможно, программа должна помогать исполняющей системе Java, ограничивая при- меняемые в ней ресурсы и освобождая приобретаемые, когда они больше не нужны. Опять же, этого можно зачастую добиться ограничением срока действия и доступности объектов и других программных конструкций. Но предусмотреть все возможные случаи нельзя, и поэ- тому следует разработать стратегию для предоставления изящного выхода из положения в качестве последнего средства.
92 Глава 2 Защитное программирование 22. Минимизируйте область действия переменных Минимизация области действия помогает разработчикам избежать типичных ошибок программирования, повышает удобочитаемость кода, связывая объявление переменных с их конкретным применением, а также упрощает сопровождение исходного кода, поскольку неиспользованные переменные легче обнаруживать и удалять. Такая мера позволяет также быстрее восстанавливать объекты средствами системы “сборки мусора” и препятствует на- рушению рекомендации 37, “Не затеняйте и не заслоняйте идентификаторы в подобластях действия”. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного программиро- вания на Java, демонстрируется объявление переменной за пределами цикла for. public class Scope { public static void main(String[] args) { int i = 0; for (i =0; i < 10; i++) { // выполнить операции } } } Приведенный выше исходный код не соответствует принятым нормам защитного про- граммирования на Java, поскольку переменная i объявляется в области действия метода, не- смотря на то, что она не используется намеренно за пределами цикла for. Один из несколь- ких сценариев, где переменную i требуется объявить в области действия метода, возникает в том случае, когда цикл содержит оператор break, а значение переменной i должно быть проверено по завершении цикла. Решение, соответствующее принятым нормам Старайтесь минимизировать область действия переменных везде, где это только возмож- но. Например, объявляйте параметры цикла в операторе for следующим образом: public class Scope { public static void main(String[] args) { for (int i = 0; i < 10; i++) { // содержит объявление // выполнить операции } } } Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного програм- мирования на Java, демонстрируется объявление переменной count за пределами метода counter (), хотя эта переменная и не используется вне этого метода. public class Foo { private int count;
22. Минимизируйте область действия переменных 93 private static final int MAX_COUNT = 10; public void counter() { count = 0; while (condition()) { /*...*/ if (count++ > MAX_COUNT) { return; } } } private boolean condition() {/* ... */} // ни в одном другом методе нет ссылки на переменную count, // но в ряде других методов имеется ссылка на переменную MAX_COUNT } Возможность повторного использования метода снижается потому, что если бы метод был скопирован в другой класс, то переменную count пришлось бы переопределять в новом контексте. Более того, метод counter () было бы труднее анализировать, поскольку анализу пришлось бы подвергнуть весь поток данных в программе, чтобы определить возможные зна- чения переменной count. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, поле count объявляется локально в методе counter (). public class Foo { private static final int MAX_COUNT = 10; public void counter() { int count = 0; while (condition()) { /*...*/ if (count++ > MAX_COUNT) { return; } } } private boolean condition () {/* ... */} // ни в одном другом методе нет ссылки на переменную count, // но в ряде других методов имеется ссылка на переменную MAX_COUNT } Применимость Обнаружение локальных переменных, объявляемых в более крупной области действия, чем требуется в прикладном коде, является простым и действенным способом, позволяющим исключить возможность появления ложно положительных результатов. Таким же простым способом является и обнаружение нескольких операторов for, в которых используется та же самая индексная переменная. Ложно положительные результаты появляются лишь в необыч- ном случае, когда значение индексной переменной предназначается для сохранения в проме- жутках между циклами.
94 Глава 2 Защитное программирование Библиография [Bloch 2001] Item 29, "Minimize the Scope of Local Variables" [JLS 2013] §14.4, "Local Variable Declaration Statements" 23. Минимизируйте область действия аннотации @SuppressWarnings Когда компилятор обнаруживает потенциальные опасности типизации данных, возника- ющие в результате смешения базовых типов с обобщенным кодом, он выдает непроверяемые предупреждения, включая непроверяемые предупреждения о приведении типов, непроверяемые предупреждения о вызове методов, непроверяемые предупреждения о создании обобщенных мас- сивов, а также непроверяемые предупреждения о преобразовании типов [Bloch 2008]. Для по- давления непроверяемых предупреждений аннотацию @SuppressWarnings ("unchecked") допускается использовать тогда и только тогда, когда код, выдающий предупреждение, га- рантированно типизирован, т.е. обеспечивает типовую безопасность. Типичным примером тому служит смешение устаревшего кода с новым клиентским кодом. Опасности, связанные с игнорированием непроверяемых предупреждений, широко обсуждаются в рекомендации “OBJ03-J. Не смешивайте обобщенные типы с необобщенными базовыми типами в новом коде” из стандарта The СЕКТ Oracle9 Secure Coding Standard for Java™ [Long 2012]. В документации на прикладной интерфейс API аннотаций типа SuppressWarnings [API 2013] по этому поводу говорится следующее: “В качестве стиля программирования эту аннотацию следует всегда использовать в на- иболее глубоко вложенном элементе, где она оказывается наиболее эффективной. Так, если требуется подавить предупреждение в отдельном методе, то аннотацией следует снабдить именно этот метод, а не его класс”. Аннотация может быть использована в объявлении переменных и методов, а также в це- лом классе. Но при этом очень важно сузить ее область действия таким образом, чтобы по- давлялись предупреждения, возникающие в суженой области действия. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного программи- рования на Java, область действия аннотации @SuppressWarnings охватывает весь класс. Приведенный ниже код опасен тем, что подавляются все непроверяемые предупреждения, возникающие в классе. Пренебрежение таким характером кода может привести к возникно- вению исключения типа ClassCastException во время выполнения. @SuppressWarnings("unchecked") class Legacy { Set s = new HashSet (); public final void doLogic(int a, char c) { s.add(a); s.add(c); // операция, не обеспечивающая типовую // безопасность, игнорируется } }
22. Минимизируйте область действия переменных 95 Решение, соответствующее принятым нормам Область действия аннотации @SuppressWarnings следует ограничить ближайшим фраг- ментом кода, где формируется предупреждение. В данном случае эта аннотация может быть использована в объявлении переменной экземпляра типа Set, как показано ниже. class Legacy { @SuppressWarnings (’'unchecked”) Set s = new HashSet () ; public final void doLogic(int a, char c) { s.add(a); // производит непроверяемое предупреждение s.add(c); // производит непроверяемое предупреждение } } Пример кода, не соответствующего принятым нормам (класс ArrayList) Данный пример кода, не соответствующего принятым нормам защитного программирова- ния на Java, взят из прежней реализации класса java.util .ArrayList. @SuppressWarnings("unchecked”) public <T> T[] toArray(T[] a) { if (a.length < size) { // производит непроверяемое предупреждение return (T[]) Arrays.copyOf(elements, size, a.getClass()); } // ... } Во время компиляции класса выдается следующее непроверяемое предупреждение о при- ведении типов: // непроверяемое предупреждение о приведении типов ArrayList.java:305: warning: [unchecked] unchecked cast found : Object[], required: T[] return (T[]) Arrays.copyOf(elements, size, a.getClass()); Данное предупреждение не может быть подавлено только для оператора return, посколь- ку это не объявление [JLS 2013]. В итоге предупреждения подавляются для всего метода в целом. И это может вызвать осложнения, когда функции, выполняющие операции, не обеспе- чивающие типовую безопасность, вводятся в метод впоследствии [Bloch 2008]. Решение, соответствующее принятым нормам (класс ArrayList) Если аннотации @SuppressWarnings нельзя использовать в подходящей области дейс- твия, как в предыдущем примере кода, не соответствующего принятым нормам, то следу- ет объявить новую переменную, чтобы хранить в ней значение и снабдить ее аннотацией ^SuppressWarnings. // ... @SuppressWarnings("unchecked") Т[] result = (Т[]) Arrays.copyOf(elements, size, a.getClass ()); return result;
9fi Глава 2 Защитное программирование Применимость Если не сократить область действия аннотации @ SuppressWarnings, это может привести к исключениям во время выполнения и нарушению гарантий типовой безопасности. Данное правило не может соблюдаться статически в полной мере. Но в некоторых особых случаях может быть использован статический анализ. Библиография [API 2013] Annotation Type SuppressWarnings [Bloch 2008] Item 24, "Eliminate Unchecked Warnings" [Long 2012] OBJ03-J. Do not mix generic with nongeneric raw types in new code 24. Минимизируйте доступность классов и их членов Классы и их члены (классы, интерфейсы, поля и методы) подлежат в Java управлению до- ступом. Порядок доступа к ним обозначается как наличием соответствующего модификатора доступа (public, protected или private), так и его отсутствием, когда доступ по умолча- нию называется закрытым пакетным доступом. В табл. 2.1 приведено упрощенное схематическое представление правил управления до- ступом. Знаком х обозначен конкретный доступ, разрешенный в данной предметной области. Например, знак х в столбце “Класс” обозначает, что член класса доступен для кода, присутс- твующего в том классе, где этот член объявлен. Аналогично знак х в столбце “Пакет” обозна- чает, что соответствующий член доступен из любого класса (или подкласса), определенного в том же самом пакете, при условии, что класс (или подкласс) загружается тем загрузчиком классов, который загружает класс, содержащий данный член. Одно и то же условие для за- грузчика классов распространяется только на закрытый пакетный доступ к членам классов. Таблица 2.1. Правила управления доступом Спецификатор доступа Класс Пакет Подкласс Внешний мир private X Отсутствует X X X* protected X X X** public X X X X ★Подклассы из одного и того же пакета могут также иметь доступ к членам, у которых от- сутствуют модификаторы доступа (по умолчанию или на уровне закрытого пакетного доступа). Дополнительные требования к доступу состоят в том, что подклассы должны загружаться тем загрузчиком классов, который загружает класс, содержащий члены с закрытым пакетным досту- пом. Подклассы из другого пакета не могут получать доступ к таким членам. ★★Для ссылки на защищенный член код, совершающий доступ к нему, должен содержаться в классе, определяющем данный защищенный член, или же в подклассе этого определяющего класса. Доступ к подклассу должен быть разрешен безотносительно к расположению подкласса в пакете. Классам и их членам должен быть предоставлен минимально допустимый доступ, что- бы у злонамеренного кода было как можно меньше возможностей нарушить безопасность.
24. Минимизируйте доступность классов и их членов 97 Классы, насколько это возможно, должны избегать раскрытия своих методов, содержащих (или вызывающих) уязвимый код через интерфейсы, которые допускают только открыто доступные методы, а такие методы являются частью открытого прикладного программного интерфейса (API) отдельного класса. (Следует иметь в виду, что данная рекомендация проти- воположна рекомендации Джошуа Блоха (Joshua Bloch) отдавать предпочтение интерфейсам над прикладными интерфейсами API [Bloch 2008, Item 16].) Исключением из этого правила служит реализация не видоизмененного интерфейса, раскрывающего открытое неизменяемое представление изменяемого класса. (См. рекомендацию “OBJ04-J. Предоставляйте изменяемые классы с функциями копирования для надежной передачи экземпляров ненадежному коду” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012].) Следует также иметь в виду, что даже если неконечный класс доступен по умолчанию, то им все же можно злоупотребить, если он содержит открытые методы. Те методы, в которых выполняются все необходимые проверки и санобработка всех вводимых данных, могут оказаться открытыми через интерфейсы. Защищенная доступность недействительна для невложенных классов, но вложенные клас- сы могут быть объявлены защищенными. Поля неконечных классов должны объявляться за- щищенными лишь в редких случаях. Ненадежный код из другого пакета может выполнить подклассификацию и получить доступ к члену класса. Более того, защищенные члены явля- ются частью прикладного интерфейса API отдельного класса, а следовательно, требуют не- прерывной поддержки. Если данное правило соблюдается, то объявлять поля защищенными не нужно. В рекомендации “OBJ01-J. Объявляйте члены данных закрытыми и предоставляйте доступные методы-оболочки” из стандарта The CERT9 Oracle9 Secure Coding Standard for Java™ [Long 2012] предлагается объявлять поля закрытыми. Если класс, интерфейс, метод или поле являются частью опубликованного прикладного интерфейса API, например, конечного пункта веб-службы, то они могут быть объявлены закрытыми или пакетно-закрытыми. Например, некритичные с точки зрения безопасности классы стимулируются к предоставлению открытых статических фабричных методов для ре- ализации управления экземплярами с помощью закрытого конструктора. Пример кода, не соответствующего принятым нормам (открытый класс) В данном примере кода, не соответствующего принятым нормам защитного программиро- вания на Java, определяется класс, являющийся внутренним для системы и не входящим ни в один из общедоступных прикладных интерфейсов API. Тем не менее этот класс объявляется как открытый. public final class Point { private final int x; private final int y; public Point (int x, int y) { this.x = x; this.у = у; } public void getPoint() { System.out.printin("(" + x + + у + ") "); } }
98 Глава 2 Защитное программирование Несмотря на то что данный пример соответствует рекомендации “OBJ06-J. Защитное ко- пирование изменяемых входных параметров и внутренних компонентов” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012], в ненадежном коде может быть по- лучен экземпляр класса Point и вызван открытый метод getPoint () для получения коор- динат точки. Решение, соответствующее принятым нормам (конечные классы с открытыми методами) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, класс Point объявляется как пакетно-закрытый в соответствии с его состоянием как не входящего ни в один из общедоступных прикладных интерфейсов API. final class Point { private final int x; private final int y; Point(int x, int y) { this.x = x; this.у = у; } public void getPoint() { System.out.printin("(" + x + у +")"); } } Класс верхнего уровня (например, Point) нельзя объявить закрытым. Пакетно-закрытая доступность оказывается приемлемой, при условии, что исключаются атаки со вставкой паке- тов. (См. рекомендацию “ENV01-J. Размещайте весь уязвимый для безопасности код в одном архивном JAR-файле, подписывая и герметизируя его” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012].) Атака co вставкой пакетов происходит во время выпол- нения, когда защищенные или пакетно-закрытые члены класса могут быть вызваны непос- редственно классом, злонамеренно вставленным в тот же самый пакет. Но совершить такую атаку трудно на практике, поскольку цель атаки и небезопасный класс должны загружаться тем же самым загрузчиком классов, помимо того, что от пакета требуется проникновение в подвергаемый атаке код. Небезопасный код, как правило, лишен подобных уровней доступа. В связи с тем что класс является конечным, метод getPoint () может быть объявлен от- крытым. Открытый подкласс, нарушающий данное правило, не может переопределить метод и раскрыть его для небезопасного кода, а следовательно, он не является доступным для такого кода. Для неконечных классов ограничение доступности методов до закрытого или пакетно- закрытого уровня исключает такую угрозу. Решение, соответствующее принятым нормам (неконечные классы с неоткрытыми методами) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, класс Point и его метод getPoint () объявляются как пакетно-за- крытые. Благодаря этому класс Point становится неконечным, а его метод getPoint () мо- жет вызываться классами, присутствующими в том же самом пакете и загружаемыми общим загрузчиком классов, как показано ниже.
24. Минимизируйте доступность классов и их членов 99 class Point { private final int x; private final int y; Point(int x, int y) { this.x = x; this.у = у; } void getPoint () { System.out.printIn("(" + x + у +’’)"); } } Пример кода, не соответствующего принятым нормам (открытый класс с открытым статическим методом) В данном примере кода, не соответствующего принятым нормам защитного программиро- вания на Java, снова определяется класс, являющийся внутренним для системы и не входящий ни в один из общедоступных прикладных интерфейсов API. Тем не менее этот класс объяв- ляется открытым. public final class Point { private static final int x = 1; private static final int у = 2; private Point (int x, int y) {} public static void getPoint () { System.out .printin (" (" + x + у +")") ; } } Код из данного примера также соответствует рекомендации “OBJ01-J. Объявляйте члены данных закрытыми и предоставляйте доступные методы-оболочки” из стандарта The CERT* Oracle9 Secure Coding Standard for Java" [Long 2012]. Тем не менее в ненадежном коде может быть получен доступ к классу Point и вызван открытый статический метод getPoint (), чтобы получить координаты точки по умолчанию. А попытка реализовать управление экзем- пляром с помощью закрытого конструктора оказывается бесполезной, поскольку открытый статический метод раскрывает внутреннее содержимое класса. Решение, соответствующее принятым нормам (пакетно-закрытый класс) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, уровень доступности класса сокращается до пакетно-закрытого. Доступ к методу getPoint () разрешается только классам, расположенным в том же самом пакете. Это препятствует вызову метода getPoint () из небезопасного кода и получению ко- ординат точки. final class Point { private static final int x = 1; private static final int у = 2;
100 Глава 2 Защитное программирование private Point (int х, int у) {} public static void getPointO { System.out.printin(”(" + x + + у + } Применимость Предоставление чрезмерного доступа нарушает инкапсуляцию и ослабляет защиту прило- жений на Java. Система с прикладным интерфейсом API, предназначенным для применения (а возможно, и расширения) в стороннем коде, должна раскрывать прикладной интерфейс API через открытый интерфейс. Потребности в таком прикладном интерфейсе API превыша- ют требования данной рекомендации. Минимальный уровень доступности к классу и его члену в любом заданном фрагменте кода может быть вычислен таким образом, чтобы исключить внедрение ошибок компиляции. Накладываемое ограничение приводит к тому, что в подобном вычислении могут быть недо- статочно учтены намерения программиста при написании кода. Например, неиспользуемые члены могут быть, очевидно, объявлены закрытыми. Но такие члены могут оказаться неис- пользуемыми только потому, что в конкретном фрагменте кода, проверяемом по случайному совпадению, может недоставать ссылок на эти члены. Тем не менее такое вычисление может послужить удобной отправной точкой для программиста, стремящегося минимизировать уровень доступа к классам и их членам. Библиография [Bloch 2008] Item 13, "Minimize the Accessibility of Classes and Members" Item 16, "Prefer Interfaces to Abstract Classes" [Campione 1996] [JLS 2013] [Long 2012] Access Control §6.6, "Access Control" ENVOI-J. Place all security-sensitive code in a single JAR and sign and seal it OBJ01-J. Declare data members as private and provide accessible wrapper methods OBJ04-J. Provide mutable classes with copy functionality to safely allow passing instances to untrusted code [McGraw 1999] Chapter 3, "Java Language Security Constructs" 25. Документируйте потоковую безопасность и пользуйтесь аннотациями везде, где только можно Языковые средства аннотирования в Java удобны для документирования целей разработ- ки. Аннотация к исходному коду является механизмом для связывания метаданных с элемен- том программы, чтобы сделать их доступными для анализа компилятором, анализаторами, отладчиками или виртуальной машиной Java (JVM). Для документирования потоковой безо- пасности или ее отсутствия имеется несколько аннотаций.
25. Документируйте потоковую безопасность и пользуйтесь аннотациями везде... 101 Получение аннотаций параллелизма Два ряда аннотаций параллелизма свободно доступны и лицензированы для применения в любом коде. Первый ряд состоит из четырех аннотаций, описанных в книге Java Concurrency in Practice (JCIP) [Goetz 2006], которую можно загрузить по адресу http://jcip.net. Анно- тации JCIP выпущены под лицензией “С указанием авторства” (Creative Commons Attribution License). Второй более обширный ряд аннотаций параллелизма предоставляется и поддерживается компанией SureLogic. Эти аннотации выпускаются под лицензией на программное обеспече- ние веб-сервера Apache (Apache Software License) версии 2.0 и могут быть загружены по адре- су www. sure logic. com. Аннотации можно проверить инструментальным средством JSure от компании SureLogic, и они остаются полезными для документирования кода, даже если дан- ное инструментальное средство недоступно. К их числу относятся аннотации JCIP, поскольку они поддерживаются инструментальным средством JSure наряду с их архивным JAR-файлом. Для того чтобы воспользоваться упомянутыми выше аннотациями, следует загрузить один или оба их архивных JAR-файла или добавить эти файлы в путь для построения кода. В последующих разделах описывается применение этих аннотаций для документирования по- токовой безопасности. Документирование намеченной потоковой безопасности В JCIP предоставляются три аннотации на уровне класса для описания целей разработки в отношении потоковой безопасности. В частности, аннотация @ThreadSafe применяется к классу для его обозначения как потокобезопасного. Это означает, что ни одна из последова- тельностей доступа (для чтения и записи в открытые поля или вызова открытых методов) не в состоянии оставить объект в неопределенном состоянии независимо от того, чередуются ли эти виды доступа с динамической или любой внешней синхронизацией или же координацией со стороны вызывающего кода. В качестве примера ниже приведен исходный код класса Aircraft, описываемого как по- токобезопасный в документации на принятые в нем правила блокировки. Этот класс защища- ет свои поля х и у с помощью реентерабельной блокировки. @ThreadSafe @Region("private Aircraftstate") @RegionLock("StateLock is stateLock protects Aircraftstate") public final class Aircraft { private final Lock stateLock = new ReentrantLock(); // ... @InRegion("Aircraftstate") private long x, y; // ... public void setPosition(long x, long y) { stateLock.lock(); try { this.x = x; this, у = у; } finally { stateLock.unlock() ; } } // ... }
102 Глава 2 Защитное программирование Аннотации ^Region и @RegionLock документируют правила блокировки, по которым утверждается обязательство сделать код потокобезопасным. Даже если использовать одну или больше аннотаций @RegionLock или @GuardedBy для документирования правил блокиров- ки класса, аннотация @ThreadSafe все равно позволяет тем, кто просматривает исходный код этого класса, интуитивно выяснить, что он является потокобезопасным. Аннотация @ Immutable применяется к неизменяемым классам. Неизменяемые объекты, по существу, являются потокобезопасными. Как только эти объекты будут полностью пос- троены, их можно опубликовать по ссылке и надежно сделать их общедоступными для ис- пользования в нескольких потоках. В приведенном ниже фрагменте кода демонстрируется неизменяемый класс Point. @Immutable public final class Point { private final int f_x; private final int f_y; public Point (int x, int y) { f_x = x; f_y = y; } public int getX() { return f_x; } public int getY() { return f_y; } } В рекомендации Джошуа Блоха [Bloch 2008] говорится следующее: “Документировать неизменяемость типов enum совсем не обязательно. Если только это не очевидно из возвращаемого типа, то статические фабричные методы должны доку- ментировать потоковую безопасность возвращаемого объекта, как демонстрируется в классе Collections. synchronizedMap”. Аннотация @NotThreadSafe применяется к классам, которые не являются потокобезо- пасными. Многие классы не в состоянии задокументировать свою безопасность для много- поточной обработки. Следовательно, программисту нелегко определить, является ли класс потокобезопасным. А данная аннотация ясно указывает на то, что классу явно недостает по- токовой безопасности. Например, большинство классов, реализующих коллекции в пакете java.util, не яв- ляются потокобезопасными. Так, в классе java .util .ArrayList потоковую безопасность можно было бы задокументировать следующим образом: package java.util.ArrayList; @NotThreadSafe public class ArrayList<E> extends ... { // ... }
25. Документируйте потоковую безопасность и пользуйтесь аннотациями везде... 103 Документирование правил блокировки Очень важно задокументировать все блокировки, применяемые для защиты разделяемого общего состояния. В этой связи Брайан Гоетц с коллегами [Goetz 2006, стр. 28] рекомендуют следующее: “Все виды доступа к изменяемой переменной состояния в более чем одном потоке должны выполняться с помощью одной и той же устанавливаемой блокировки. В этом случае можно утверждать, что переменная защищена данной блокировкой”. Для этой цели предоставляется аннотация @GuardedBy от JCIP, а также аннота- ция @RegionLock от компании SureLogic. Поле или метод, к которому применяется ан- нотация @GuardedBy, могут быть доступны только при установке конкретной блоки- ровки. Это может быть внутренняя или динамическая блокировка, как, например, в пакете java.util. concurrent. В приведенном ниже примере демонстрируется класс MovablePoint, реализующий перемещаемую точку, способную запоминать свое прежнее местоположение, используя списочный массив memo. @ThreadSafe public final class MovablePoint { @GuardedBy("this") double xPos = 1.0; @GuardedBy("this") double yPos = 1.0; @GuardedBy("itself") static final List<MovablePoint> memo = new ArrayList<MovablePoint>(); public void move(double slope, double distance) { synchronized (this) { rememberPoint(this); xPos += (1 / slope) * distance; yPos += slope * distance; } } public static void rememberPoint(MovablePoint value) { synchronized (memo) { memo.add(value) ; } } } В аннотациях @GuardedBy к полям xPos и yPos указывается, что доступ к этим полям защищен установкой блокировки текущего объекта this. Метод move (), вносящий измене- ния в эти поля, также синхронизирован с текущим объектом this. В то же время аннота- ция @GuardedBy к списочному массиву memo указывает на то, что блокировка объекта типа ArrayList защищает его содержимое. Метод rememberPoint () также синхронизирован со списочным массивом memo. Но недостаток аннотации @GuardedBy заключается в том, что она неспособна указы- вать на отсутствие отношений между полями в классе. Впрочем, этот недостаток можно преодолеть с помощью аннотации @RegionLock от компании SureLogic, где объявляется
104 Глава 2 Защитное программирование новая региональная блокировка класса, к которому применяется данная аннотация. В ре- зультате такого объявления устанавливается новая именованная блокировка, связываю- щая конкретный объект блокировки с отдельной областью класса, которая может быть доступна только при установке блокировки. В приведенном ниже примере кода правила блокировки в классе SimpleLock обозначают, что синхронизация с экземпляром это- го класса защищает все его состояние. В отличие от аннотации @GuardedBy, аннотация @RegionLock позволяет программисту присвоить явное (и желательно содержательное) имя правилу блокировки. @RegionLock("SimpleLock is this protects Instance") class Simple { . . . } Помимо именования правила блокировки, аннотация @Region позволяет присвоить имя области защищаемого состояния. Такое имя ясно дает понять, что состояние и правило бло- кировки подходят друг другу, как показано в следующем примере: ^Region("private Aircraftposition") @RegionLock("StateLock is stateLock protects Aircraftposition") public final class Aircraft { private final Lock stateLock = new ReentrantLockO; @InRegion("AircraftPosition") private long x, y; @InRegion("Aircraftposition") private long altitude; // ... public void setPosition(long x, long y) { stateLock.lock(); try { this.x = x; this.у = у; } finally { stateLock.unlock() ; } } // ... } В данном примере правило блокировки под названием StateLock служит для ука- зания на то, что блокировка объекта типа stateLock защищает область под названием AircraftPosition, в которую входит изменяемое состояние, используемое для обозначения местоположения самолета. Построение изменяемых объектов Обычно построение объекта считается исключением из правила блокировки, поскольку при первоначальном создании объекты привязываются к потоку. Объект привязывается к тому потоку, в котором создается его экземпляр с помощью оператора new. После создания объекта его можно благополучно опубликовать в других потоках. Но объект не станет об- щим до тех пор, пока это не будет разрешено в том потоке, где создан экземпляр данного объекта. Способы надежной публикации объектов, обсуждаемые в рекомендации “TSM01-J.
25. Документируйте потоковую безопасность и пользуйтесь аннотациями везде... 105 Не допускайте исчезновения ссылки this при построении объекта” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012], могут быть кратко выражены с помощью аннотации @Unique ("return"). Например, в следующем фрагменте кода аннотация ^Unique ("return") документирует, что объект возвращается из конструктора в виде однозначной ссылки: GRegionLock("Lock is this protects Instance") public final class Example { private int x = 1; private int y; @Unique("return") public Example(int y) { this.у = у; } // ... } Документирование правил привязки к потоку Дин Сазерленд и Уильям Шерлис (William Scherlis) предлагают аннотации, позволяющие документировать правила привязки к потоку. Их подход состоит в том, чтобы разрешить про- верку аннотаций относительно написанного кода [Sutherland 2010]. В приведенном ниже при- мере кода аннотации выражают следующую цель разработки: в программе должен быть хотя бы один поток диспетчеризации событий из библиотеки AWT (Abstract Window Toolkit — абстрактно-оконный инструментарий) и несколько вычислительных потоков, в которых за- прещено обрабатывать структуры данных или события AWT. @ThreadRole AWT, Compute QlncompatibleThreadRoles AWT, Compute @MaxRoleCount AWT 1 Документирование протоколов ожидания и уведомления Брайан Гоетц с коллегами [Goetz 2006, стр. 395] рекомендуют следующее: “Класс, зависящий от состояния, должен полностью раскрыть (и задокументировать) протоколы ожидания и уведомления в своих подклассах или вообще запретить им участие в этих протоколах. (Это расширение принципа “проектирования и документи- рования наследования, а иначе — его запрещения” [EJ Item 15].) По крайней мере, раз- работка класса, зависящего от состояния, для целей наследования требует раскрытия условных очередей и блокировок и документирования условных предикатов и правил синхронизации. Она может также потребовать раскрытия базовых переменных состо- яния. (В худшем случае класс, зависящий от состояния, может раскрыть свое состоя- ние подклассам, но не документировать свои протоколы для ожидания и уведомления. Это все равно, как если бы класс раскрыл свои переменные состояния, но не задоку- ментировал свои инварианты.)” Протоколы ожидания и уведомления должны быть задокументированы надлежащим образом. В настоящее время еще неизвестны аннотации, предназначенные для этой цели.
106 Глава 2 Защитное программирование Применимость Аннотирование параллельно выполняемого кода помогает правильно задокументировать цель разработки и может быть использовано для автоматизации процесса обнаружения и предотвращения условий возникновения гонок, в том числе и гонок данных. Библиография [Bloch 2008] [Goetz 2006] [Long 2012] [Sutherland 2010] Item 70, "Document Thread Safety" Java Concurrency in Practice TSM01-J. Do not let the this reference escape during object construction "Composable Thread Coloring" 26. Всегда предоставляйте отклик на результирующее значение метода Методы следует разрабатывать таким образом, чтобы возвращать значение, позволяющее разработчику выяснить текущее состояние объекта и/или результат операции. Данная реко- мендация согласуется с рекомендацией “EXP00-J. Не пренебрегайте значениями, возвращае- мыми методами” из стандарта The CERT* Oracle9 Secure Coding Standard for Java"1 [Long 2012]. Возвращаемое значение должно представлять последнее известное состояние и выбираться с учетом восприятия и ментальной модели разработчика. Отклик можно также предоставить, сгенерировав стандартное или специальное исклю- чение в виде объекта класса, производного от класса Exception. При таком подходе разра- ботчик может по-прежнему получать точную информацию о результате выполнения метода и предпринимать далее необходимые действия. Для этого исключение должно предоставить подробный отчет о ненормальном условии на соответствующем уровне абстракции. Такие подходы должны применяться в прикладных интерфейсах API в определенном со- четании, чтобы помочь клиентам отличить правильные результаты от неправильных и сти- мулировать тщательную обработку любых неправильных результатов. В тех случаях, когда имеется общепринятый код ошибки, который просто невозможно интерпретировать как достоверное значение, возвращаемое из метода, должен быть возвращен именно этот код ошибки. А в остальных случаях должно быть сгенерировано исключение. Метод не должен возвращать значение, состоящее как из самого возвращаемого значения, так из кода ошибки (подробнее об этом см. в рекомендации 52 “Избегайте внутренних индикаторов ошибок”). С другой стороны, объект может предоставить метод проверки состояния [Bloch 2008], в котором проверяется, находится ли объект в согласованном состоянии. Такой подход приме- ним только в тех случаях, когда состояние объекта не может быть видоизменено во внешних потоках. Благодаря этому исключается возникновение условия гонок типа “время провер- ки — время использования” (TOCTOU) между вызовом метода, проверяющего состояние объекта, а также вызовом метода, зависящего от состояния данного объекта. В промежутке между этими вызовами состояние объекта может измениться неожиданно или даже злона- меренно. Значения или коды ошибок, возвращаемые из методов, должны точно обозначать состоя- ние объекта на определенном уровне абстракции. Клиенты должны быть в состоянии выпол- нять решающие действия, опираясь на возвращаемое значение.
26. Всегда предоставляйте отклик на результирующее значение метода 107 Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного программи- рования на Java, метод updateNode () видоизменяет узел древовидной структуры, если ему удастся обнаружить данный узел в связном списке, а иначе — он ничего с ним не делает. public void updateNode (int id, int newValue) { Node current = root; while (current != null) { if (current.getld() == id) { current.setValue(newValue); break; } current = current.next; } } Этому методу не удается указать, что он видоизменил любой узел. Следовательно, в вы- зывающем коде нельзя определить, был ли метод выполнен успешно или неудачно, но неза- метно. Решение, соответствующее принятым нормам (возврат логического значения) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, в качестве результата операции возвращается логическое значение true, если узел древовидной структуры был видоизменен, а иначе — логическое значение false. public boolean updateNode (int id, int newValue) { Node current = root; while (current != null) { if (current.getId() == id) { current.setValue(newValue); return true; // узел успешно обновлен } current = current.next; } return false; } Решение, соответствующее принятым нормам (генерирование исключения) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, видоизмененный объект типа Node возвращается в том случае, если он обнаружен. А если искомый узел недоступен в списке, то генерируется исключение типа NodeNotFoundException. public Node updateNode (int id, int newValue) throws NodeNotFoundException { Node current = root; while (current != null) {
108 Глава 2 Защитное программирование if (current.getId () == id) { current.setValue(newValue); return current; ) current = current.next; } throw new NodeNotFoundException () ; } Применение исключений для указания сбоя в программе может стать удачным проектным решением, но генерирование исключений не всегда оказывается уместным. В целом метод должен генерировать исключение только в том случае, если предполагается его успешное за- вершение, но при этом возникают неисправимые ошибочные ситуации, или же в том случае, если предполагается, что ошибка должна быть исправлена в методе, вызываемом из класса, расположенного вверх по иерархии. Решение, соответствующее принятым нормам (возврат пустого значения) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, возвращается обновленный объект типа Node, чтобы разработчик мог просто проверить пустое возвращаемое значение, если операция завершится неудачно. public Node updateNode(int id, int newValue) { Node current = root; while (current != null) { if (current.getId() == id) { current.setValue(newValue); return current; } current = current.next; } return null; } Возвращаемое значение, которое может быть пустым (null), представляет собой внут- ренний индикатор ошибки, более подробно рассматриваемый в рекомендации 52 “Избегайте внутренних индикаторов ошибок” Такое проектное решение допустимо, но считается под- чиненным другим проектным решениям, в том числе и другим решениям, соответствующим принятым нормам и представленным в данной рекомендации. Применимость Если не предоставить подходящий отклик в виде определенного сочетания возвращаемых значений, кодов ошибок и исключений, то объект может перейти в неопределенное состоя- ние, а поведение программы станет непредсказуемым. Библиография [Bloch 2008] Item 59. Avoid unnecessary use of checked exceptions [Long 2012] EXPOO-J. Do not ignore values returned by methods [Ware 2008] Writing Secure Java Code
27. Распознавайте файлы, используя несколько файловых атрибутов 109 27. Распознавайте файлы, используя несколько файловых атрибутов Многие уязвимости в защите, имеющие отношение к файлам, возникают в результате того, что программа получает доступ не к тому файловому объекту. Это нередко происходит по- тому, что имена файлов лишь слабо связаны с базовыми файловыми объектами. Ведь имена файлов не предоставляют сведения, касающиеся характера самого файлового объекта. Более того, привязка имени файла к файловому объекту пересматривается всякий раз, когда имя файла используется в операции. Такой пересмотр может привести к возникновению в прило- жении условия гонки типа “время проверки — время использования” (TOCTOU). Объекты типа java. io. File и java. nio. file. Path привязываются к базовым файловым объектам на уровне операционной системы только тогда, когда осуществляется доступ к файлу. Конструкторы и методы renameTo () и delete () из класса java. io.File опираются только на имена файлов для их распознавания. Это же относится и к методам get () из клас- са java.nio. file.Path, move () и delete () из класса java.nio. file.Files. Поэтому всеми этими методами следует пользоваться крайне осторожно. Правда, файлы можно зачастую распознавать по другим атрибутам, помимо имени фай- ла. Их, например, можно сравнивать по времени создания или модификации. Сведения о со- зданном или закрытом файле могут быть сохранены, а затем использованы для проверки до- стоверности подлинности файла, если его требуется открыть снова. А сравнение нескольких атрибутов увеличивает вероятность того, что повторно открываемый файл окажется тем же самым, что и файл, открывавшийся прежде. Распознавание файлов имеет меньшее значение для приложений, где файлы хранятся в безопасных каталогах, откуда они могут быть доступны только для их владельца, а возможно, и системного администратора (см. рекомендацию “FIOOO-J. Не оперируйте файлами в общих каталогах” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012]). Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного программи- рования на Java, файл распознается по символьной строке с его именем, чтобы выяснить, был ли он открыт, обработан, закрыт и затем еще раз открыт для чтения. public void processFile(String filename) { // распознать файл из пути к нему Path filel = Paths.get(filename); // открыть файл для записи try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(filel)))) { // записать в файл. . . } catch (lOException e) { // обработать ошибку } // закрыть файл /* * Возникающее здесь условие гонок позволяет совершающему * атаку злоумышленнику сменить один файл на другой */
110 Глава 2 Защитное программирование // еще раз открыть файл для чтения Path file2 = Paths.get(filename); try (BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(file2)))) { String line; while ((line = br.readLine()) != null) { System.out.printIn(line); } } catch (lOException e) { // обработать ошибку } } Привязка имени файла к базовому файловому объекту пересматривается при создании объекта типа BufferedReader, и поэтому в данном примере кода нельзя гарантировать, что файл, открытый для чтения, окажется тем же самым, что и файл, отрывавшийся ранее для записи. В этом случае совершающий атаку злоумышленник может заменить исходный файл (например, символической ссылкой) в промежутке между первым вызовом метода close () и последующим созданием объекта типа BufferedReader. Пример кода, не соответствующего принятым нормам (вызов метода Files. isSameFile ()) В данном примере кода, не соответствующего принятым нормам защитного програм- мирования на Java, предпринимается попытка обеспечить открытие для чтения того же самого файла, который открывался ранее для записи. И с этой целью вызывается метод Files.isSameFile(). public void processFile (String filename) { // распознать файл из пути к нему Path filel = Paths.get(filename); // открыть файл для записи try (BufferedWriter bw = new BufferedWriter (new OutputStreamWriter(Files.newOutputStream(filel)))) { // записать в файл... } catch (lOException e) { // обработать ошибку } // ... // еще раз открыть файл для чтения Path file2 = Paths.get(filename); if (!Files.isSameFile(filel, file2)) { // файл был заменен, обработать ошибку } try (BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(file2)))) { String line; while ((line = br.readLine ()) != null) { System.out.printin(line);
27. Распознавайте файлы, используя несколько файловых атрибутов 111 } catch (lOException е) { // обработать ошибку } } К сожалению, в прикладном интерфейсе API языка Java не гарантируется, что в мето- де isSameFile () на самом деле производится проверка, является ли файл тем же самым. Так, в документации на прикладной интерфейс API версии Java 7 в отношении метода isSameFile () говорится следующее: “Если оба объекта типа Path равны, то данный метод возвращает логическое значение true, даже не проверяя, существует ли файл” Это означает, что метод isSameFile () может просто проверить, одинаковы ли пути к обоим сравниваемым файлам. Но при этом он не в состоянии обнаружить, был ли файл по данному пути заменен другим файлом в промежутке между двумя последовательными опера- циями его открытия. Решение, соответствующее принятым нормам (проверка с помощью нескольких атрибутов) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, проверяются даты создания и последнего видоизменения файлов с целью увеличить вероятность того, что файл, открывавшийся для чтения, окажется тем же самым, что и файл, открывавшийся для записи. public void processFile(String filename) throws IOException{ / / распознать файл из пути к нему Path filel = Paths.get(filename); BasicFileAttributes attrl = Files.readAttributes(filel, BasicFileAttributes.class); FileTime creationl = attrl.creationTime(); FileTime modifiedl = attrl.lastModifiedTime(); // открыть файл для записи try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(filel)))) { // записать в файл... } catch (lOException e) { // обработать ошибку } // еще раз открыть файл для чтения Path file2 = Paths.get(filename); BasicFileAttributes attr2 = Files.readAttributes(file2, BasicFileAttributes.class); FileTime creation2 = attr2.creationTime(); FileTime modified2 = attr2.lastModifiedTime(); if ( (!creationl.equals(creation2)) || (!modifiedl.equals(modified2)) ) { // файл был заменен, обработать ошибку } try (BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(file2)))){
112 Глава 2 Защитное программирование String line; while ((line = br.readLine ()) != null) { System.out.printin(line); } } catch (lOException e) { // обработать ошибку } } Несмотря на то что данное решение является достаточно безопасным, решительный зло- умышленник, совершающий атаку, может создать символическую ссылку с теми же самыми датами создания и последнего видоизменения, что и у исходного файла. Кроме того, условие гонки типа “время проверки — время использования” (TOCTOU) может возникнуть в про- межутке между моментами первоначального чтения атрибутов файла и его первого открытия. Аналогично, еще одно условие гонки типа “время проверки — время использования” может возникнуть в промежутке между моментами вторичного чтения атрибутов файла и его пов- торного открытия. Решение, соответствующее принятым нормам (проверка с помощью атрибута f ileKey по стандарту POSIX) В средах, поддерживающих атрибут f ileKey, применяется более надежный подход к про- верке одинаковости атрибутов f ileKey двух сравниваемых файлов. Атрибут f ileKey пред- ставляет собой объект, который “однозначно определяет файл” [API 2013], как показано в представленном здесь решении, соответствующем принятым нормам защитного программи- рования на Java. public void processFile (String filename) throws IOException{ // распознать файл из пути к нему Path filel = Paths.get(filename); BasicFileAttributes attrl = Files.readAttributes(filel, BasicFileAttributes.class); Object keyl = attrl.fileKey(); // открыть файл для записи try (BufferedWriter bw = new BufferedWriter ( new OutputStreamWriter(Files.newOutputStream(filel)))) { /I записать в файл... } catch (lOException e) { // обработать ошибку } // еще раз открыть файл для чтения Path file2 = Paths.get(filename); BasicFileAttributes attr2 = Files.readAttributes(file2, BasicFileAttributes.class); Object key2 = attr2.fileKey(); if ( !keyl.equals(key2) ) { System.out.printIn ("File tampered with"); // файл был заменен, обработать ошибку } try (Buf feredReader br = new Buf feredReader ( new InputStreamReader(Files.newInputStream(file2)))) {
27. Распознавайте файлы, используя несколько файловых атрибутов 113 String line; while ((line = br.readLine()) != null) { System.out.printin(line); } } catch (lOException e) { 11 обработать ошибку } } Такой поход годится не для всех платформ. Например, в версии Windows 7 Enterprise Edition все атрибуты f ileKey являются пустыми. Однозначность файлового ключа, возвра- щаемого методом fileKey (), гарантируется только в том случае, если файловая система и файлы остаются статическими. Например, идентификатор файла может повторно использо- ваться в файловой системе после его удаления. Как и в предыдущем совместимом решении, в промежутке между моментами первоначального чтения атрибутов файла и его первого открытия может возникнуть условие гонки типа “время проверки — время использования” (TOCTOU). А еще одно такое же условие гонки может возникнуть в промежутке между мо- ментами вторичного чтения атрибутов файла и его повторного открытия. Решение, соответствующее принятым нормам (применение класса RandomAccessFile) Более совершенный подход заключается в том, чтобы избежать повторного открытия фай- ла. В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, демонстрируется применение класса RandomAccessFile, который позволяет открывать файл как для чтения, так и для записи. Благодаря тому, что файл закры- вается только автоматически в операторе try с ресурсами, исключается возникновение ус- ловия гонок. Следует, однако, иметь в виду, что в этом и других решениях, соответствующих принятым нормам защитного программирования на Java, метод readLine () применяется только ради наглядности примера. А о возможных слабых сторонах этого метода см. в реко- мендации “MSC05-J. Не исчерпывайте область динамической памяти” из стандарта The СЕКТ Oracle9 Secure Coding Standard for Java"' [Long 2012]. public void processFile (String filename) throws IOException{ 11 распознать файл из пути к нему try ( RandomAccessFile file = new RandomAccessFile(filename, "rw")) { 11 записать в файл... // вернуться в начало файла и прочитать его содержимое file.seek(0); String line; while ((line = file.readLine()) != null) { System.out.printin(line); } } } Пример кода, не соответствующего принятым нормам (проверка размера файла) В данном примере кода, не соответствующего принятым нормам защитного программиро- вания на Java, предпринимается попытка убедиться, что открываемый файл содержит точно 1024 байта.
114 Глава 2 « Защитное программирование static long goodsize = 1024; public void doSomethingWithFile(String filename) { long size = new File (filename) . length () ; if (size != goodsize) { System.out.printin("File has wrong size!"); return; } try (Buf feredReader br = new Buf feredReader ( newInputStreamReader(new Fileinputstream(filename)))) { // ... обработать файл } catch (lOException e) { // обработать ошибку } } В приведенном выше коде может возникнуть условие гонки типа “время проверки — вре- мя использования” (TOCTOU) в промежутке между моментами проверки размера файла и его открытия. Если совершающий атаку злоумышленник заменит файл размером 1024 байта другим файлом в промежутке, когда может возникнуть данное условие гонки, то программа способна открыть фактически любой файл в обход проверки его размера. Решение, соответствующее принятым нормам (проверка размера файла) В представленном здесь решении, соответствующем принятым нормам защитного програм- мирования на Java, для получения размера файла применяется метод FileChannel. size (). А поскольку этот метод применяется к объекту типа Fileinputstream только после откры- тия файла, то данное решение исключает появление окна для гонок. static long goodsize = 1024; public void doSomethingWithFile(String filename) { try (Fileinputstream in = new Fileinputstream(filename); BufferedReader br = new BufferedReader ( new InputStreamReader(in))) { long size = in.getChannel(). size(); if (size != goodsize) { System.out.printin("File has wrong size!"); return; } String line; while ((line = br.readLine()) != null) { System.out.printin(line); } } catch (lOException e) { // обработать ошибку } }
28. Не присоединяйте значимость к порядковому значению... 115 Применимость Совершающие атаки нередко используют уязвимости в защите, имеющие отношение к файлам, чтобы заставить программу получить доступ не к тому файлу. Для того чтобы пре- дотвратить использование подобных уязвимостей в защите, требуется организовать надлежа- щее распознавание файлов. Библиография [API 2013] Class java. io. File Interface java. nio. file. Path Class java. nio. file. Files Interface java. nio. file. attribute.BasicFileAttributes [Long 2012] FIOOO-J. Do not operate on files in shared directories 28. He присоединяйте значимость к порядковому значению, связанному с перечислением Для перечислимых типов в языке Java имеется метод ordinal (), возвращающий число- вую позицию каждой константы перечислимого типа в объявлении ее класса. В документации на прикладной интерфейс API языка Java [API 2013] по этому поводу говорится следующее: “Метод public final int ordinal () из обобщенного класса Enum<E extends Enum<E>> возвращает константу перечислимого типа (ее позицию в объявлении пере- числения, где исходной константе присваивается нулевое порядковое значение). Боль- шинство программистов вряд ли будут пользоваться этим методом. Ведь он предназна- чен для применения в сложных структурах данных на основе перечислений, включая EnumSet и EnumMap”. В §8.9 “Перечисления” спецификации языка Java (JLS) [JLS 2013] не предписывается применение метода ordinal () в программах. Но присоединение значимости к порядково- му значению константы перечислимого типа, возвращаемому методом ordinal (), чревато ошибками, а следовательно, нежелательно в защитном программировании. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного программиро- вания на Java, объявляется перечисление Hydrocarbon, а его метод ordinal () служит для того, чтобы предоставить результат вызова метода getNumberOfCarbons (). enum Hydrocarbon { METHANE, ETHANE, PROPANE, BUTANE, PENTANE, HEXANE, HEPTANE, OCTANE, NONANE, DECANE; public int getNumberOfCarbons() { return ordinal() + 1; } }
116 Глава 2 « Защитное программирование И хотя код из данного примера, не соответствующего принятым нормам защитного программирования на Java, ведет себя так, как и предполагалось, сопровождать его сов- сем не просто. Так, если переставить константы в перечислении Hydrocarbon, метод getNumberOfCarbons () возвратит неверные значения. Более того, ввод дополнительной константы BENZENE в перечисление может нарушить инвариант, предполагаемый методом getNumberOfCarbons (), поскольку у бензола имеется шесть атомов углерода, а порядковое значение шесть уже присвоено гексану. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, константы перечислимого типа явно связываются с соответствующи- ми целочисленными значениями, обозначающими количество атомов углерода. enum Hydrocarbon { METHANE(1), ETHANE(2), PROPANE(3), BUTANE(4), PENTANE(5), HEXANE(6), BENZENE(6), HEPTANE(7), OCTANE(8), NONANE(9), DECANE(10); private final int numberOfCarbons; Hydrocarbon(int carbons) { this.numberOfCarbons = carbons; } public int getNumberOfCarbons () { return numberOfCarbons; } } Метод ordinal () больше не применяется в методе getNumberOfCarbons () для обнару- жения количества атомов углерода, соответствующих каждому порядковому значению. С од- ним и тем же порядковым значением могут быть связаны разные константы, как показано на примерах констант HEXANE и BENZENE. Более того, в таком решении отсутствует всякая зависимость от порядка перечисления. Метод getNumberOfCarbons () будет и дальше дейс- твовать правильно, даже если переупорядочить перечисление. Применимость Когда порядок констант в перечислении стандартный, а дополнительные константы в него не вводятся, то вполне допустимо пользоваться порядковыми значениями, связанными с пе- речислимым типом. Например, применение порядковых значений допускается в приведенном ниже перечислимом типе. Как правило, применение порядковых значений для выведения це- лых значений затрудняет сопровождение программы и может привести к ошибкам в про- грамме. public enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY } Библиография [API 2013] [Bloch 2008] [JLS 2013] Class Enum<E extends Enum<E» Item 31, "Use Instance Fields Instead of Ordinals" §8.9,"Enums"
29. Принимайте во внимание числовое продвижение типов 117 29. Принимайте во внимание числовое продвижение типов Числовое продвижение типов служит для преобразования операндов числового операто- ра к общему типу, позволяющему выполнить операцию. Когда в арифметических операторах употребляются операнды разного размера, то более узкие операнды продвигаются к типу бо- лее широких операндов. Правила продвижения типов В §5.6 “Числовые продвижения типов” спецификации JLS [JLS 2013] описываются следую- щие правила числового продвижения типов. 1. Если любые операнды относятся к ссылочному типу, то выполняется преобразование с распаковкой. 2. Если один из операндов относится к типу double, то другой операнд преобразуется в тип double. 3. Иначе если один из операндов относится к типу float, то другой операнд преобразу- ется в тип float. 4. Иначе если один из операндов относится к типу long, то другой операнд преобразу- ется в тип long. 5. Иначе оба операнда преобразуются в тип int. Расширение преобразований, получающееся в результате числового продвижения типов, позволяет сохранить общую величину числа. Но продвижение типов, в котором операнды преобразуются из типа int в тип float или из типа long в тип double, могут привести к потере точности. (Подробнее об этом см. в рекомендации “NUM 13-J. Избегайте потери точ- ности при преобразовании примитивных целых значений в значения с плавающей точкой” из стандарта The СЕКТ Oracle* Secure Coding Standard for Java™ [Long 2012].) Подобные преобразования могут происходить при использовании операторов умножения и деления (%, *, /), сложения и вычитания (+, -), сравнения (<, >, <=, >=), равенства (=, ?=) и целочисленных поразрядных операторов (&, |, А). Примеры В следующем примере происходит продвижение к типу double перед применением ариф- метического оператора +: int а = одно_значение; double Ь = другое_значение; double с = а + b; В приведенном ниже примере кода значение переменной b сначала преобразуется в тип int, чтобы применить оператор + к операндам одного и того же типа. Затем результат опера- ции (а + Ь) преобразуется в тип float, и наконец, применяется оператор сравнения. int а = некоторое_значение; char b = некоторый_символ; if ((а + b) > l.lf) { // сделать что-нибудь
118 Глава 2 Защитное программирование Составные операторы Если в составных выражениях употребляются операторы смешанных типов, то может про- изойти приведение типов. К числу составных операторов присваивания относятся операторы +=, -=, *=, /=, &=, А=, %=, «=, »=, »>= и |= В §15.26.2 “Составные операторы присваива- ния” спецификации JLS [JLS 2013] утверждается следующее: “Составное выражение присваивания в форме El ор= Е2 равнозначно выражению El = (Т) ((El) op (Е2) ), где Т обозначает тип выражения Е1, за исключением того, что тип выражения Е1 вычисляется только один раз”. Это означает, что в составном выражении присваивания производится неявное приве- дение типа результата вычисления к типу левого операнда. Если же операнды относятся к разным типам, то может произойти несколько преобразований. Так, если выражение Е1 от- носится к типу int, а выражение Е2 — к типу long, float или double, то сначала происхо- дит расширение типа выражения Е1 до типа выражения Е2 (до выполнения оператора ор), а затем следует сужение типа выражения Е2 обратно до типа int (после выполнения оператора ор, но перед присваиванием). Пример кода, не соответствующего принятым нормам (умножение) В данном примере кода, не соответствующего принятым нормам защитного программи- рования на Java, значение переменной big типа int умножается на значение переменной one типа float. int big = 1999999999; float one = l.Of; // двоичная операция, в ходе которой происходит потеря // точности вследствие неявного приведения типов System.out.printin(big * one); В данном случае числовое продвижение типов требуется для того, чтобы продвинуть тип переменной big к типу float, прежде чем произойдет умножение. И в конечном ито- ге это приводит к потере точности, т.е. в данном примере кода в качестве результата умно- жения переменных выводится значение 2.0Е9, а не 1.999999999Е9. (См. рекомендацию “NUM 13-J. Избегайте потери точности при преобразовании примитивных целых значений в значения с плавающей точкой” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012].) Решение, соответствующее принятым нормам (умножение) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, вместо типа float употребляется тип double как более безопасное средство расширенного преобразования примитивных типов, происходящего в результате це- лочисленного продвижения типов. int big = 1999999999; double one = 1. Od; 11 объявить тип double вместо типа float System.out.printin(big * one);
29. Принимайте во внимание числовое продвижение типов 119 Такое решение дает предполагаемый результат 1.999999999Е9, т.е. значение, получаемое при присваивании значения типа int значению типа double с предварительно выполняемым неявным приведением типов. Подробнее о смешении арифметических операций над целыми значениями и значениями с плавающей точкой см. в рекомендации 60 “Преобразуйте целые значения в значения с плавающей точкой для выполнения операций с плавающей точкой”. Пример кода, не соответствующего принятым нормам (сдвиг влево) В данном примере кода, не соответствующего принятым нормам защитного программиро- вания на Java, демонстрируется целочисленное продвижение типов, возникающее в результате выполнения поразрядного логического оператора ИЛИ. byte [ ] b = new byte [ 4 ] ; int result = 0; for (int i = 0; i < 4; i++) { result = (result « 8) | b[i]; } Каждый элемент байтового массива расширяется до 32 бит и дополняется знаком перед использованием в качестве операнда. Так, если бы этот элемент массива первоначально со- держал значение Oxff, то в конечном итоге он содержал бы значение Oxffffffff [FindBugs 2008]. Таким образом, результирующее значение отличается от сцепления четырех элементов массива. Решение, соответствующее принятым нормам (сдвиг влево) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, для достижения предполагаемого результата маскируются старшие 24 бита в значении элемента байтового массива. byte[] b = new byte[4]; int result = 0; for (int i = 0; i < 4; i++) { result = (result « 8) | (b[i) & Oxff); } Пример кода, не соответствующего принятым нормам (составная операция сложения и присваивания) В данном примере кода, не соответствующего принятым нормам защитного программиро- вания на Java, выполняется составная операция сложения и присваивания. int х = 2147483642; // 0x7ffffffa х += l.Of; // переменная х содержит значение 2147483647 // (0x7fffffff) после вычисления Составная операция включает в себя значение типа int, содержащее слишком много битов, чтобы их можно было вместить в 23-разрядной мантиссе числового значения типа float, поддерживаемого в Java. Это приводит к расширяющему преобразованию типа int в тип float с потерей точности. Зачастую результирующее значение оказывается не тем, что ожидается.
120 Глава 2 Защитное программирование Решение, соответствующее принятым нормам (составная операция сложения и присваивания) Для целей защитного программирования следует избегать употребления любых составных операторов присваивания значений переменным типа byte, short или char. Следует также воздерживаться от применения более широкого операнда в правой части выражения. В пред- ставленном здесь решении, соответствующем принятым нормам защитного программирова- ния на Java, все операнды относятся к поддерживаемому в Java типу double. double х = 2147483642; 11 0x7ffffffa x += 1.0; // переменная x содержит значение 2147483643.0 // (0x7ffffffb.0), как и предполагалось Пример кода, не соответствующего принятым нормам (составная операция поразрядного сдвига и присваивания) В данном примере кода, не соответствующего принятым нормам защитного программи- рования на Java, применяется составной оператор сдвига вправо с целью сдвинуть значение переменной i на один разряд. short i = -1; i »>= 1; К сожалению, значение переменной i остается прежним. Сначала значение переменной i продвигается к типу int. Это приводит к расширяющему преобразованию примитивных типов, а следовательно, к потере точности. Если коротко, то значение -1 получает двоичное представление Oxf f f f. В результате преобразования в тип int получается двоичное значение Oxf f f f f f f f, которое затем сдвигается вправо на 1 бит, чтобы получилось двоичное значение 0x7fffffff. Для того чтобы сохранить это значение обратно в переменной i типа short, в Java выполняется неявное сужающее преобразование типов, в ходе которого отбрасываются 16 старших битов. В конечном итоге получается то же самое двоичное значение Oxf f ff или десятичное значение -1. Решение, соответствующее принятым нормам (составная операция поразрядного сдвига и присваивания) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, составной оператор присваивания применяется к типу int, и для это- го не требуется расширение и последующее сужение преобразования типов. Следовательно, переменная i получает значение 0x7 fffffff. int i = -1; i »>= 1; Применимость Если не принять во внимание правила продвижения типов в целочисленных операндах, а также в операндах с плавающей точкой, то в конечном итоге это может привести к потере точности.
30. Активизируйте проверку типов в методах с переменным количеством... 121 Библиография [Bloch 2005] Р [Findbugs 2008] [JLS 2013] [Long 2012] Puzzle 9, "Tweedledum" Puzzle 31, "Ghost of Looper" "BIT: Bitwise OR of Signed Byte Value" §4.2.2, "Integer Operations" §5.6, "Numeric Promotions" §15.26.2, "Compound Assignment Operators" NUM13-J. Avoid loss of precision when converting primitive integers to floating-point 30. Активизируйте проверку типов в методах с переменным количеством аргументов во время компиляции Методом с переменным количеством аргументов (или varargs) называется такой метод, который может принимать нефиксированное количество аргументов. Тем не менее у него должен быть хотя бы один фиксированный аргумент. При обработке вызова метода с пере- менным количеством аргументов компилятор Java проверяет типы всех его аргументов. При этом все эти аргументы должны соответствовать формальному типу переменного количества аргументов. Но проверка типов во время компиляции оказывается неэффективной, если в методе применяются параметры обобщенных типов или типа Object [Bloch 2008]. Для того чтобы активизировать строгую проверку типов в методах с переменным количеством аргу- ментов во время компиляции, следует указать как можно более конкретный тип данных для параметров таких методов. Пример кода, не соответствующего принятым нормам (тип Object) В данном примере кода, не соответствующего принятым нормам защитного программи- рования на Java, ряд числовых значений суммируется в методе с переменным количеством аргументов типа Object. Следовательно, этот метод принимает произвольное сочетание па- раметров в виде разнотипных объектов. Такое объявление параметров метода редко употреб- ляется как допустимое, хотя из этого правила имеются исключения, упоминаемые в разделе “Применимость” данной рекомендации. double sum(Object... args) { double result = 0.0; for (Object arg : args) { if (arg instanceof Byte) { result += ((Byte) arg).byteValue (); } else if (arg instanceof Short) { result += ((Short) arg).shortValue(); } else if (arg instanceof Integer) { result += ((Integer) arg).intValue(); } else if (arg instanceof Long) { result += ((Long) arg) .longValueO ; } else if (arg instanceof Float) { result += ((Float) arg).floatvalue(); } else if (arg instanceof Double) {
122 Глава 2 Защитное программирование result += ((Double) arg).doubleValue (); } else { throw new ClassCastException(); } } return result; } Решение, соответствующее принятым нормам (тип Number) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, определяется тот же самый метод, но для объявления его аргументов используется тип Number. Это тип абстрактного класса, который является в достаточной сте- пени обобщенным, чтобы охватить все числовые типы данных, но в то же время исключить нечисловые типы данных. double sum(Number... args) { // ... } Пример кода, не соответствующего принятым нормам (обобщенный тип) В данном примере кода, не соответствующего принятым нормам защитного программи- рования на Java, объявляется тот же самый метод с переменным количеством аргументов, но на этот раз — обобщенного типа. Этот метод принимает переменное количество параметров, причем все они относятся к одному и тому же типу объектов, хотя и могут представлять раз- нотипные объекты. Опять же, такое объявление параметров метода редко употребляется как допустимое. <Т> double sum(T... args) { // ... } Решение, соответствующее принятым нормам (обобщенный тип) В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, определяется тот же самый метод, а для объявления его аргументов используется тип Number. < Т extends Number> double sum(T... args) { // ... } При объявлении параметров метода с переменным количеством аргументов следует как можно конкретнее определять их типы, избегая типа Object и неконкретности обобщенных типов. Переделка старых методов с параметрами в виде конечного массива на методы с пере- менным количеством параметров обобщенного типа не всегда оказывается целесообразной. Если, например, метод не принимает аргумент конкретного типа, то проверку его типа во время компиляции можно было бы заменить на переменное количество параметров обоб- щенного типа, чтобы метод был скомпилирован скорее чисто, чем правильно. Но это привело бы к ошибке во время выполнения [Bloch 2008].
31. Не объявляйте открытыми и конечными константы... 123 Следует также иметь в виду, что автоупаковка препятствует строгой проверке прими- тивных типов и соответствующих им классов-оболочек во время компиляции. Например, в представленном здесь решении, соответствующем принятым нормам защитного программи- рования на Java, выдается приведенное ниже предупреждение, хотя код действует так, как и предполагалось. Этим конкретным предупреждением компилятора можно благополучно пренебречь. Java.java:10: warning: [unchecked] Possible heap pollution from parameterized vararg type T < T extends Number> double sum(T... args) { Применимость Опрометчивое употребление типов в методах с переменным количеством аргументов пре- пятствует организации строгой проверки типов во время компиляции. Оно приводит к неод- нозначности и делает менее удобочитаемым исходный код. Сигнатуры методов с переменным количеством аргументов, в которых используется тип Object и неточные обобщенные типы, оказываются вполне пригодными в тех случаях, когда в теле метода отсутствуют операции приведения типов и автоупаковки, а его исходный код компилируется без ошибок. Рассмотрим следующий пример кода, корректно действующего для всех типов объектов и успешно выполняющего проверки типов: < Т> Collection<T> assembleCollection (Т. . . args) { return new HashSet<T>(Arrays.asList (args)); } В некоторых случаях для переменного числа аргументов приходится употреблять тип Object. Характерным тому примером служит метод j ava . util. Formatter . format (String format, Object. . . args), способный отформатировать объекты любо- го типа. В этом случае автоматическое обнаружение объектов осуществляется напрямую. Библиография [Bloch 2008] [Steinberg 2008] [Oracle 2011b] Item 42, "Use Varargs Judiciously" Using the Varargs Language Feature Varargs 31. He объявляйте открытыми и конечными константы, значения которых могут измениться в последующих выпусках программы С помощью ключевого слова final можно указать значения констант, т.е. значения, ко- торые не изменяются во время выполнения программы. Но константы, которые могут быть изменены в течение срока эксплуатации программы, не следует объявлять открытыми и ко- нечными (public final). В спецификации JLS [JLS 2013] допускается встраивание значения любого открытого конечного поля в любую единицу компиляции, где осуществляется чтение этого поля. Следовательно, если при редактировании объявления класса в новой версии от- крытому конечному полю присваивается новое значение, то единицам компиляции, где это поле читается, может быть по-прежнему доступно старое значение до тех пор, пока они не
124 Глава 2 и Защитное программирование будут перекомпилированы. Подобное затруднение может, например, возникнуть в том случае, когда произойдет переход к последней версии обновленной сторонней библиотеки, а обраща- ющийся к ней код не перекомпилирован. Аналогичная ошибка может возникнут и в том случае, если объявляется статическая ко- нечная (static final) ссылка на изменяемый объект. Подробнее об этом см. в рекоменда- ции 73 “Не путайте неизменяемость ссылки и доступного по ссылке объекта”. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного программи- рования на Java, в классе Foo из пакета Foo. j ava объявляется поле, значение которого обоз- начает версию программы. class Foo { public static final int VERSION = 1; // ... } А в дальнейшем доступ к этому осуществляется из класса Ваг в отдельной единице ком- пиляции (пакете Bar .java), как показано ниже. class Ваг { public static void main(String[] args) { System.out.printin("You are using version " + Foo.VERSION); } } После компиляции и запуска программы на выполнение будет правильно выведено сле- дующее сообщение: You are using version 1 (Вы пользуетесь версией 1) Но если разработчик заменит значение в поле VERSION на 2, видоизменив и перекомпили- ровав пакет Foo. j ava, но забыв перекомпилировать заодно и пакет Bar. j ava, то программа неправильно выведет прежнее сообщение: You are using version 1 Этот недостаток нетрудно исправить, перекомпилировав пакет Bar. java. Но имеется и лучшее решение. Решение, соответствующее принятым нормам В §13.4.9 “Конечные поля и константы” спецификации JLS [JLS 2013] говорится следующее: “Кроме истинных математических констант, переменные классов рекомендуется объ- являть статическими и конечными (static final) в исходном коде лишь в самом крайнем случае. Если же требуется конечная переменная только для чтения, то ее луч- ше объявить закрытой и статической (private static), чтобы получить ее значение подходящим методом доступа”. В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, поле version объявляется в пакете Foo. java как закрытое и стати- ческое (private static), а доступ к нему осуществляется методом getVersion ().
31. Не объявляйте открытыми и конечными константы... 125 class Foo { private static int version = 1; public static final int getVersionO { return version; } // ... } В класс Bar из пакета Bar. java вносятся изменения, чтобы вызывать метод getVersion () и получать значение поля version из пакета Foo .java, как показано ниже. class Ваг { public static void main(String[] args) { System.out.printIn("You are using version " + Foo.getVersion()); } } В данном решении значение закрытого поля version нельзя скопировать в класс Ваг во время компиляции, и благодаря этому устраняется упомянутая выше программная ошибка. Следует, однако, иметь в виду, что подобное преобразование едва ли скажется на производи- тельности, поскольку большинство динамических (JIT) генераторов кода в состоянии встро- ить метод getVersion () во время выполнения. Применимость Объявление значения типа final, изменяющегося в течение срока эксплуатации програм- мы, может привести к неожиданным результатам. В §9.3, “Объявления полей и констант” спе- цификации JLS [JLS 2013] по этому поводу говорится следующее: “Каждое поле неявно объявляется в теле интерфейса как открытое, статическое и ко- нечное (public static final). В объявлении такого поля дополнительно разреша- ется указывать любые или все модификаторы”. Следовательно, данная рекомендация не распространяется на поля, определяемые в ин- терфейсах. Очевидно, что если значение поля в интерфейсе изменится, то каждый класс, в котором реализуется и применяется этот интерфейс, должен быть перекомпилирован. Под- робнее об этом см. в рекомендации 35 “Тщательно разрабатывайте интерфейсы, прежде чем их выпускать”. Данная рекомендация не распространяется на константы, объявляемые как перечислимые типа enum. А константы, значения которых вообще не изменяются в течение срока действия программы, могут быть объявлены как конечные (final). Например, математические конс- танты в спецификации JLS рекомендуется объявлять открытыми. Библиография [JLS 2013] §4.12.4, "final Variables" §8.3.1.1, "static Fields" §9.3, "Field (Constant) Declarations" §13.4.9, "final Fields and Constants"
126 Глава 2 Защитное программирование 32. Избегайте циклических зависимостей пакетов В книге The Elements of Java™ Style [Allen 2000] и стандарте JPL Java Coding Standard [Havelund 2009] указывается следующее требование: структура зависимостей пакета не долж- на содержать циклы. Это означает, что она должна быть такой же представительной, как и направленный ациклический граф (DAG). Исключение циклических зависимостей пакетов дает ряд преимуществ. Тестирование и сопровождение исходного кода. Циклические зависимости усилива- ют последствия от изменения и вставки “заплат” в исходный код. А уменьшение пос- ледствий от этих изменений упрощает тестирование и облегчает сопровождение ис- ходного кода. Неспособность выполнять надлежащее тестирование из-за циклических зависимостей нередко служит источником уязвимостей в защите. Повторное использование. Циклические зависимости пакетов требуют синхронного выпуска и обновления пакетов. Но данное требование уменьшает возможность пов- торного использования пакетов. Выпуски и сборки. Исключение циклических зависимостей помогает также перено- сить разработку в среду, где поощряется разбиение на модули. Развертывание. Исключение циклических зависимостей пакетов сокращает их свя- зывание. Сокращение связывания пакетов приводит к снижению ошибок во время выполнения, например, ошибки типа ClassNotFoundError. А это, в свою очередь, упрощает развертывание программ. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного программи- рования на Java, содержатся пакеты account и user, состоящие из классов AccountHolder, User и UserDetails соответственно. В частности, класс UserDetails расширяет класс AccountHolder, поскольку пользователь выступает в роли владельца счета. Класс AccountHolder зависит от нестатического служебного метода, определенного в классе User. Аналогично класс UserDetails зависит от класса AccountHolder, расширяя его. package account; import user.User; public class AccountHolder { private User user; public void setUser(User newUser) {user = newUser;} synchronized void depositFunds (String username, double amount) { // использовать служебный метод из класса User для проверки // существования пользователя с заданным именем if (user.exists(username)) { // внести сумму на счет } } protected double getBalance(String accountNumber) { // возвратить остаток на счете return 1.0; } }
32. Избегайте циклических зависимостей пакетов 127 package user; import account.AccountHolder; public class UserDetails extends AccountHolder { public synchronized double getUserBalance(String accountNumber) { // использовать метод из класса AccountHolder для // получения остатка на счете return getBalance(accountNumber); } } public class User { public boolean exists(String username) { // проверить существование пользователя с заданным именем return true; 11 такой пользователь существует } } Решение, соответствующее принятым нормам Тесное связывание классов в двух пакетах можно ослабить, внедрив интерфейс BankApplication в третий пакет bank. Циклическая зависимость пакетов исключается га- рантией того, что класс AccountHolder не зависит от класса User, а вместо этого опирается на интерфейс, импортируя пакет bank, но не реализуя интерфейс BankApplication. В представленном здесь решении, соответствующем принятым нормам защитного програм- мирования на Java, подобные функциональные возможности достигаются введением парамет- ра интерфейса типа BankApplication в метод deposit Funds (). Такое решение дает классу AccountHolder надежный контракт с пакетом bank. Кроме того, в классе UserDetails пре- доставляются конкретные реализации методов из интерфейса BankApplication и в то же время наследуются другие методы из класса AccountHolder. package bank; public interface BankApplication { void depositFunds(BankApplication ba, String username, double amount); double getBalance(String accountNumber); double getUserBalance(String accountNumber); boolean exists(String username); } package account; import bank.BankApplication; // импортировать из третьего пакета class AccountHolder { private BankApplication ba; public void setBankApplication(BankApplication newBA) { ba = newBA; } public synchronized void depositFunds (BankApplication ba, String username, double amount) { // использовать служебный метод из класса UserDetails для // проверки существования пользователя по заданному имени if (ba.exists(username)) { 11 внести сумму на счет } }
128 Глава 2 Защитное программирование public double getBalance(String accountNumber) { // возвратить остаток на счете return 1.0; package user; import account.AccountHolder; // односторонняя зависимость import bank.BankApplication; // импортировать из третьего пакета public class UserDetails extends AccountHolder implements BankApplication { public synchronized double getUserBalance(String accountNumber) { // использовать метод из класса AccountHolder для получения // остатка на счете return getBalance(accountNumber); } public boolean exists(String username) { 11 проверить существование пользователя по заданному имени return true; Оказывается, что интерфейс BankApplication содержит лишние методы, в том числе deposit Funds () и getBalance (). А поскольку эти методы присутствуют в данном интер- фейсе, то если они переопределяются в подклассе, то в суперклассе остается возможность для внутреннего вызова методов из подкласса по принципу полиморфизма. Например, метод ba.getBalance () можно вызвать с его переопределенной реализацией из класса UserDetails. Одно из последствий такого решения состоит в том, что методы, объявленные в интерфейсе, должны быть непременно открытыми в тех классах, где они определяются. Применимость Циклические зависимости пакетов могут стать причиной прочности формируемых сбо- рок. Уязвимость защиты в одном пакете может легко проникнуть в другие пакеты. Библиография [Allen 2000] [Havelund 2009] [Knoernschild 2002] The Elements of Java™ Style JPL Coding Standard, Version 1.1 Chapter 1,"OO Principles and Patterns" 33. Отдавайте предпочтение определяемым пользователем исключениям над более общими типами исключений Исключение перехватываются по его типу, и поэтому их лучше всего определять для кон- кретных целей, вместо того чтобы использовать общие типы исключений для многих целей. Генерирование исключений общих типов затрудняет понимание и сопровождение исходного кода, а также нивелирует большую часть преимуществ механизма обработки исключений в Java.
33. Отдавайте предпочтение определяемым пользователем... 129 Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного програм- мирования на Java, предпринимается попытка провести различие между разными видами поведения исключений, исходя из анализа сообщения об исключении. Так, если в методе doSomething () генерируется исключение или ошибка типа, относящегося к подклассу, про- изводному от класса Throwable, то в операторе switch можно выбрать конкретный вариант для дальнейшего выполнения кода обработки исключений и ошибок. Например, если получа- ется сообщение об исключении “Файл не найден”, то в коде обработки исключений выполня- ется соответствующее действие. try { doSomething() ; } catch (Throwable e) { String msg = e.getMessage () ; switch (msg) { case ’’file not found": // обработать ошибку break; case "connection timeout": // обработать ошибку break; case "security violation": // обработать ошибку break; default: throw e; } } Тем не менее любое изменение в литералах, используемых в сообщении об исключении, приведет к нарушению нормальной работы кода. Допустим, что выполняется следующая строка кода: throw new Exception ("cannot find file") ; Генерируемое в ней исключение должно быть обработано в первой ветви case оператора switch. Но вместо этого исключение будет сгенерировано повторно, поскольку символьная строка сообщения не совпадает ни с одним из условий в ветвях case оператора switch. Более того, исключения могут быть сгенерированы и без уведомляющих о них сообщений. Данный пример кода, не соответствующего принятым нормам защитного программирования на Java, подпадает под действие правила обработки исключений типа ERR08-EX0 из рекомен- дации “ERR08-J. Не перехватывайте исключение типа NullPointerException и любые его предшественники” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012], поскольку общие исключения не только перехватываются в нем, но и генерируются повторно. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, применяются конкретные типы исключений, а там, где требуется, определяются новые типы исключений специального назначения. public class TimeoutException extends Exception { TimeoutException () {
130 Глава 2 Защитное программирование super(); } TimeoutException (String msg) { super(msg); } } // ... try { doSomething(); } catch (FileNotFoundException e) { // обработать ошибку } catch (TimeoutException te) { // обработать ошибку } catch (SecurityException se) { // обработать ошибку Применимость Исключения служат для обработки исключительных ситуаций. Если исключение не пе- рехватывается, то выполнение программы прекращается. Исключение, перехваченное непра- вильно или не на том уровне восстановления работоспособности кода, зачастую приводит к неверному поведению. Библиография [JLS 2013] Chapter 11, "Exceptions" [Long 2012] ERR08-J. Do not catch NullPointerException or any of its ancestors 34. Старайтесь изящно исправлять системные ошибки По поводу исключений в §11.1.1 “Виды исключений” спецификации JLS [JLS 2013] гово- рится следующее: “Непроверяемые исключения относятся к классу RuntimeException и его подклас- сам, а также к классу Error и его подклассам. Все остальные исключения относятся к классам проверяемых исключений”. Классы непроверяемых исключений не подвергаются проверке во время компиляции, поскольку на данной стадии очень трудно учесть все исключительные ситуации, тогда как восстановить работоспособность кода нередко оказывается затруднительно, а то и вообще невозможно. Но даже в том случае, если восстановить работоспособность кода невозмож- но, виртуальная машина Java (JVM) предоставляет изящный выход из этого затруднитель- ного положения и возможность хотя бы зарегистрировать ошибку. Такая возможность появ- ляется при использовании блока try-catch, в котором перехватывается исключение типа Throwable. Перехват исключения типа Throwable разрешается и в том случае, если в коде требуется исключить утечку потенциально уязвимой информации. Во всех остальных случаях
34. Старайтесь изящно исправлять системные ошибки 131 перехват исключения типа Throwable не рекомендуется, поскольку он затрудняет обработку конкретных исключений. А в тех случаях, когда могут быть выполнены операции очистки вроде освобождения системных ресурсов, в коде должен быть использован блок finally или оператор try с ресурсами для освобождения ресурсов. Перехват исключения типа Throwable вообще запрещается в рекомендации “ERR08-J. Не перехватывайте исключение типа NullPointerException и любые его предшественни- ки” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]. Но он все же разрешается, если организуется фильтрация исключений по правилу обработки исключений типа ERR08-EX0 из данной рекомендации. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного программи- рования на Java, ошибка типа StackOverf lowError генерируется в результате бесконечной рекурсии. Ведь в противном случае наступает исчерпание оперативной памяти, выделяемой под стек, а следовательно, и отказ в обслуживании. public class Stackoverflow { public static void main(String[] args) { infiniteRun(); // ... private static void infiniteRun() { infiniteRun(); Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам защитного про- граммирования на Java, демонстрируется применение блока try-cat ch для перехвата ошибок типа java.lang.Error или исключений типа java.lang.Throwable. В блоке try-catch может быть сделана длинная запись в журнале регистрации, а затем предприняты попытки освободить главные системные ресурсы в блоке finally. public class Stackoverflow { public static void main(String[] args) { try { infiniteRun(); } catch (Throwable t) { // передать управление обработчику исключений } finally { // освободить кеш и ресурсы } // ... } private static void infiniteRun () { infiniteRun(); } }
132 Глава 2 Защитное программирование Следует, однако, иметь в виду, что код передачи управления обработчику исключений должен работать корректно в условиях ограниченного объема оперативной памяти, посколь- ку стек или динамическая область памяти (так называемая “куча”) оказывается близкой к полному исчерпанию. Во избежание подобных ситуаций имеет смысл заблаговременно заре- зервировать память, специально предназначенную для обработчика исключений, связанных с исчерпанием оперативной памяти. Следует также иметь в виду, что в данном решении исключение типа Throwable пере- хватывается при попытке обработать ошибку. И это согласуется с правилом обработки ис- ключений типа ERR08-EX2 из рекомендации “ERR08-J. Не перехватывайте исключение типа Null PointerExcept ion и любые его предшественники” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]. Применимость Если предоставить системе возможность внезапно прекратить выполнение программы на Java, то может возникнуть уязвимость к нарушению безопасности типа отказа в обслу- живании. Если же оперативная память исчерпывается, то, скорее всего, некоторые данные программы оказываются в неопределенном состоянии. Поэтому данный процесс лучше всего перезапустить. Если же предпринять попытку продолжить выполнение кода, то в качестве эффективного обходного приема можно порекомендовать сокращение количества потоков. Такая мера может оказаться действенной в подобных случаях, поскольку в потоках нередко возникают утечки памяти, а для дальнейшего их существования может потребоваться допол- нительный объем оперативной памяти. Для обработки ошибок типа OutOfMemoryError в потоках могут быть использованы методы Thread. setUncaughtExceptionHandler () и ThreadGroup.uncaughtException(). Библиография [JLS 2013] §11.2, "Compile-Time Checking of Exceptions" [Kalinovsky 2004] Chapter 16, "Intercepting Control Flow: Intercepting System Errors" [Long 2012] ERR08-J. Do not catch NullPointerException or any of its ancestors 35. Тщательно разрабатывайте интерфейсы, прежде чем их выпускать Интерфейсы служат для группирования всех методов, которые класс обязуется сделать от- крытыми. В частности, классы, реализующие интерфейс, обязаны предоставить конкретные реализации всех его методов. Интерфейсы являются неотъемлемой составляющей большинс- тва общедоступных прикладных интерфейсов API. После их выпуска очень трудно устранить обнаруженные в них недостатки, не нарушив исходный код, реализующий прежние их вер- сии. Ниже перечислены некоторые последствия некачественной разработки интерфейсов. Изменения, вносимые в интерфейс с целью устранить обнаруженные в нем ошиб- ки, могут серьезно нарушить контракты с классами, реализующими этот интерфейс. Например, устранение ошибки в последующей версии интерфейса может повлечь за собой видоизменения в несвязанном с ним интерфейсе, который теперь придется ре- ализовать клиенту. Но клиенту может быть запрещено устранять ошибку, поскольку новый интерфейс может наложить на него дополнительное бремя своей реализации.
35. Тщательно разрабатывайте интерфейсы, прежде чем их выпускать 133 Средства реализации могут предоставить своим клиентам исходные или скелетные ре- ализации методов интерфейса для их последующего расширения. Но такой код может отрицательно сказаться на поведении подклассов. Следовательно, в отсутствие подоб- ных исходных реализаций подклассы должны предоставить фиктивные реализации, стимулирующие формирование среды разработки, где нередки комментарии вроде “игнорировать этот код и ничего не делать”. Такой код может быть вообще не протес- тирован. Если обнаруживается уязвимость в защите общедоступного прикладного интерфейса API (см., например, обсуждение методов из класса ThreadGroup в рекомендации “THI01-J. Не вызывайте методы из класса ThreadGroup” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]), то она будет сохраняться в течение всего срока действия дан- ного прикладного интерфейса API, оказывая отрицательное воздействие на безопасность любого приложения или библиотеки, в котором этот прикладной интерфейс применяется. И даже после полного или частичного устранения данной уязвимости в защите общедоступ- ного прикладного интерфейса API его небезопасная версия будет по-прежнему применяться в приложениях и библиотеках до тех пор, пока они не будут сами обновлены. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам защитного программиро- вания на Java, интерфейс User закреплен со следующими двумя методами: authenticate () и subscribe (). А в дальнейшем поставщики предоставляют бесплатно выпускаемую услугу, не опирающуюся на аутентификацию. public interface User { boolean authenticate(String username, chart] password); void subscribe(int noOfDays); // внедрено после открытого выпуска класса void freeService() ; } К сожалению, ввод метода freeService () нарушает весь клиентский код, реализующий интерфейс. Более того, средствам реализации, стремящимся воспользоваться только методом freeService (), придется предоставить и два других метода, а это “засоряет” прикладной интерфейс API по описанным выше причинам. Пример кода, не соответствующего принятым нормам Другой замысел состоит в том, чтобы отдать предпочтение абстрактным классам для ре- шения задачи постоянного развития констант. Но для этого придется пожертвовать гибкос- тью интерфейсов, благодаря которой в классе можно реализовать несколько интерфейсов, но расширить только один класс. К числу примечательных шаблонов для решения подобной за- дачи относится распространение поставщиком абстрактного скелетного класса, реализующего развивающийся интерфейс. Такой скелетный класс может выборочно реализовывать одни ме- тоды и принудительно расширять классы для предоставления конкретных реализаций других методов. Так, если в интерфейс вводится новый метод, скелетный класс может предоставить неабстрактную по умолчанию реализацию метода, который может быть дополнительно пере- определен в расширяющем классе. И такой скелетный класс демонстрируется в приведенном ниже примере кода, не соответствующего принятым нормам защитного программирования на Java.
134 Глава 2 Защитное программирование public interface User { boolean authenticate(String username, chart] password); void subscribe(int noOfDays); void freeService(); // внедрено после открытого выпуска // прикладного интерфейса API } abstract class SkeletalUser implements User { public abstract boolean authenticate(String username, char[] password); public abstract void subscribe(int noOfDays); public void freeService() { // добавить позднее, предоставить реализацию //и еще раз выпустить класс } } class Client extends SkeletalUser { // реализовать методы authenticate() и subscribe(), // но не метод freeService() } Несмотря на всю пользу от такого шаблона, он может оказаться небезопасным, поскольку поставщик, не имеющий представления об исходном коде расширяющего класса, может вы- брать реализацию, вносящую уязвимости в защиту клиентского прикладного интерфейса API. Решение, соответствующее принятым нормам (разбиение на модули) Более совершенное проектное решение состоит в том, чтобы заранее предусмотреть пер- спективное развитие предоставляемых услут. Базовые функциональные возможности долж- ны быть реализованы в интерфейсе User. В качестве их расширения в данном случае может потребоваться только срочная услуга. И для того чтобы воспользоваться новой бесплатной услугой, в существующем классе может быть затем выбрано одно из двух: реализация нового интерфейса FreeUser или полное игнорирование этой услуги. public interface User { boolean authenticate(String username, char[] password); } public interface PremiumUser extends User { void subscribe(int noOfDays); } public interface FreeUser { void freeService(); } Решение, соответствующее принятым нормам (превращение нового метода в неиспользуемый) Другое решение, соответствующее принятым нормам защитного программирования на Java, состоит в том, чтобы сгенерировать исключение в новом методе freeService (), опре- деленном в реализующем его подклассе.
36. Пишите код, удобный для "сборки мусора" 135 class Client implements User { public void freeService() { throw new AbstractMethodError (); } } Решение, соответствующее принятым нормам (делегирование реализации новых методов подклассам) Еще одно возможное, хотя и менее гибкое решение, соответствующее принятым нормам защитного программирования на Java, состоит в том, чтобы делегировать реализацию метода подклассам, производным от класса, в котором реализуется базовый интерфейс. abstract class Client implements User { public abstract void freeService(); // делегировать реализацию нового метода подклассам // Другие конкретные реализации } Применимость Если не опубликовать устойчивые, безукоризненные интерфейсы, то могут быть наруше- ны контракты с реализующими их классами, “засорен” прикладной интерфейс API, а возмож- но, и внесены уязвимости в защиту реализующих их классов. Библиография [Bloch 2008] Item 18, "Prefer Interfaces to Abstract Classes" [Long 2012] THI01 -J. Do not invoke ThreadGroup methods 36. Пишите код, удобный для "сборки мусора" Система “сборки мусора” в Java предоставляет значительные преимущества по сравне- нию с другими языками программирования, где такая система отсутствует. Система “сборки мусора” предназначена для автоматического освобождения недоступной области памяти и предотвращения утечек памяти. Несмотря на то что такая система вполне способна спра- виться с подобной задачей, злоумышленник может начать атаку типа отказа в обслуживании на систему “сборки мусора”, внедрив, в частности, код для аномального выделения динами- ческой области памяти или чрезмерно продолжительного удерживания объектов в оператив- ной памяти. Например, в некоторых версиях системы “сборки мусора” может потребоваться остановка всех исполняющихся процессов для своевременной обработки входящих запросов на выделение оперативной памяти, приводящее к повышению интенсивности операций по управлению динамической областью памяти. И в этом случае резко падает производитель- ность системы. В частности, системы реального времени уязвимы к более изощренной атаке типа отказа в обслуживании, проникающей в систему путем кражи циклов центрального процессора (ЦП) и медленно приводящей в постепенному исчерпанию оперативной памяти. Совершающий атаку злоумышленник может распределить оперативную память таким образом, чтобы резко увеличить потребление системных ресурсов (например, ЦП, энергии заряда аккумуляторной
136 Глава 2 Защитное программирование батареи и оперативной памяти), не вызывая ошибку типа OutOfMemoryError. Написание кода, удобного для “сборки мусора”, ограничивает многие пути для проникновения подобных атак в систему. Применяйте неизменяемые объекты с коротким сроком действия Начиная с версии JDK 1.2 затраты на выделение памяти в системе “сборки мусора разных поколений” сокращены, как правило, до уровня, ниже чем в языке С или C++. Сокращение подобных затрат в системе “сборки мусора разных поколений” достигается благодаря груп- пированию объектов по отдельным поколениям, причем более молодое поколение состоит из объектов с коротким сроком действия. Система “сборки мусора” освобождает оперативную память от молодого поколения уже недействующих объектов [Oracle 2010а]. Усовершенство- ванные алгоритмы “сборки мусора” позволяют сократить затраты на “сборку мусора” пропор- ционально количеству действующих объектов в молодом поколении, а не количеству объек- тов, для которых была выделена оперативная память, начиная с момента последней “сборки мусора”. Следует, однако, иметь в виду, что объекты из молодого поколения, долго сохраняющие- ся в оперативной памяти, считаются долгоживущими и переводятся в разряд долгоживущего поколения. Лишь немногие объекты из молодого поколения продолжают существовать до сле- дующего цикла “сборки мусора”, а остальные подготавливаются к “сборке мусора” в предсто- ящем цикле [Oracle 2010а]. Благодаря системе “сборки мусора разных поколений” применение неизменяемых объек- тов с коротким сроком действия, как правило, оказывается более эффективным, чем изменяе- мых объектов с длительным сроком действия, в том числе и пула объектов. Исключение пула объектов повышает эффективность системы “сборки мусора”. Пулы объектов влекут за собой дополнительные затраты и риски, поскольку они способны затруднить синхронизацию и мо- гут потребовать явного управления процессом освобождения оперативной памяти от объек- тов, а также вызвать осложнения, связанные с появлением висячих указателей. Кроме того, определение оптимального объема оперативной памяти для резервирования пула объектов может быть затруднено, особенно это касается ответственного кода. Поэтому применение изменяемых объектов с длительным сроком действия остается приемлемым в тех случаях, когда выделение оперативной памяти для объектов оказывается особенно затратным (например, при соединении нескольких таблиц из разных баз данных). Аналогично пулы объ- ектов являются подходящим проектным решением, когда объекты представляют ограничен- ные ресурсы, например, пулы потоков и соединений с базой данных. Избегайте крупных объектов Выделение оперативной памяти для крупных объектов является затратной операцией от- части потому, что затраты на инициализацию их полей оказываются пропорциональными их размерам. Кроме того, частое выделение оперативной памяти для крупных объектов разных размеров может вызвать их фрагментацию или затруднить выполнение уплотняющих опера- ций “сборки мусора”. Не вызывайте систему "сборки мусора" явным образом Систему “сборки мусора” можно вызвать явным образом с помощью метода System, gc (). И хотя в документации говорится, что при вызове данного метода выполняется “сборка
36. Пишите код, удобный для "сборки мусора' 137 мусора” в то же время никак не гарантируется, что “сборка мусора” вообще будет выполнена и когда именно это произойдет. На самом деле вызов метода System.gc () позволяет лишь предположить, что “сборка мусора” будет выполнена впоследствии. А виртуальная машина JVM может вполне проигнорировать такое предположение. Таким образом, безответственное использование такой возможности способно значи- тельно понизить производительность системы, если “сборка мусора” будет начинаться в неподходящие моменты, а не ожидать удобных моментов, когда можно будет благопо- лучно произвести “сборку мусора” без существенного вмешательства в ход выполнения программы. В виртуальной машине Java Hotspot VM, применяемой по умолчанию, начиная с версии JDK 1.2, вызов метода System, gc () принуждает к явной “сборке мусора”. Подобные вызовы могут быть глубоко скрыты в недрах библиотек, что затрудняет их отслеживание. Для того чтобы проигнорировать вызов системы “сборки мусора” в подобных случаях, следует исполь- зовать параметр командной строки -XX:+DisableExplicitGC. А во избежание длительных приостановок на время выполнения полной “сборки мусора” может быть запущен менее от- ветственный параллельный цикл с помощью параметра командной строки -XX:ExplicitGC InvokedConcurrent. Применимость Злоупотребление системой “сборки мусора” может привести к значительному снижению производительности. И это обстоятельство может быть использовано для атаки типа отка- за в обслуживании. Уязвимость GERONIMO-4574 в веб-серверах Apache Geronimo и Tomcat, о которой сообщалось в марте 2009 года, возникла в результате того, что объекты данных, предназначавшиеся для обработки средствами класса PolicyContext, устанавливались в по- токе и вообще не освобождались, и в конечном итоге они оставались в оперативной памяти дольше, чем требовалось. Когда приложение проходит несколько стадий, включая инициализацию и подготовку к работе, в промежутках между этими стадиями может потребоваться уплотнение динамичес- кой области памяти. В подобных случаях может быть вызван метод System, gc (), при ус- ловии, что в промежутках между стадиями наступает подходящий момент, не отмеченный никакими примечательными событиями. Библиография [API 2013] [Bloch 2008] [Coomes 2007] [Goetz 2004] [Lo 2005] [Long 2012] [Oracle 2010a] Class System Item 6, "Eliminate Obsolete Object References" "Garbage Collection Concepts and Programming Tips" Java Theory and Practice: Garbage Collection and Performance "Security Issues in Garbage Collection" OBJ05-J. Defensively copy private mutable class members before returning their references OBJ06-J. Defensively copy mutable inputs and mutable internal components Java SE 6 HotSpot™ Virtual Machine Garbage Collection Tuning
Надежность В стандарте ISO/IEC/IEEE 24765:2010 “Словарь терминов по разработке систем и про- граммного обеспечения” надежность определяется как способность системы или ее компо- нента выполнять требуемые функции при заданных условиях в течение указанного периода времени [ISO/IEC/IEEE 24765:2010]. А в стандарте ISO/IEC 9126-1:2001 “Разработка програм- много обеспечения, качество продукции. Часть 1. Модель качества” предоставляется ана- логичное определение надежности как способности программного продукта поддерживать указанный уровень производительности при его использовании в заданных условиях [ISO/ IEC 9126-1:2001]. Надежность программного обеспечения играет важную роль в обеспечении надежности системы в целом. Она отличается от надежности аппаратных средств тем, что отражает со- вершенство разработки, а не изготовления. Ограничения надежности возникают вследствие просчетов в технических требованиях, разработке и реализации. А отказы, возникающие из- за этих просчетов, зависят скорее от способа эксплуатации программного продукта и выбира- емых параметров настройки программы, чем от фактической продолжительности ее работы. В стандарте ISO/IEC/IEEE 24765:2010 надежность программного обеспечения определяет- ся как вероятность того, что оно не приведет к отказу системы в течение указанного периода времени при заданных условиях [ISO/IEC/IEEE 24765:2010]. А вероятность является функ- цией не только входных данных системы и ее применения, но и существования недочетов в самом программном обеспечении. Данные, вводимые в систему, определяют проявление в ней дефектов, если таковые существуют. А повышение сложности программного обеспечения служит главным фактором, оказывающим влияние на его надежность. Рекомендации, представленные в этой главе, имеют отношение к языковым средствам Java, которыми легко злоупотребить по опрометчивости. Язык Java довольно гибок в приме- нении, но это его свойство может привести к появлению неясных методик программирования и кода, который трудно понять и сопровождать. Следуя рекомендациям, представленным в этой главе, программирующие на Java могут получать код, в меньшей степени подверженный программным ошибкам и сбоям во время выполнения.
140 Глава 3 Надежность В этой главе содержатся рекомендации, нацеленные на следующее. 1. Оказать помощь в сокращении ошибок, а следовательно, и в разработке надежного кода на Java. 2. Дать конкретные наставления по программированию на Java с целью повысить надеж- ность программного обеспечения. 37. Не затеняйте и не заслоняйте идентификаторы в подобластях действия Повторное использование имен идентификаторов в подобластях действия приводит к тому, что они затеняются или заслоняются. Идентификаторы, повторно используемые в теку- щей области действия, могут сделать недоступными идентификаторы, определенные в каком- нибудь другом месте. И хотя в спецификации языка Java (JLS) [JLS 2013] ясно разрешается лю- бая синтаксическая неоднозначность, возникающая в результате затенения или заслонения, подобная неоднозначность затрудняет сопровождение и ревизию исходного кода, особенно в тех случаях, когда в коде требуется доступ к первоначально именованному и недоступному компоненту. Дело еще больше усложняется, когда повторно используемое имя определено в другом пакете. В §6.4.2 “Заслонение” спецификации JLS [JLS 2013] говорится следующее: “Простое имя может возникнуть в контекстах, где существует вероятность интерпре- тировать его как имя переменной, типа данных или пакета. В подобных случаях прави- ла, установленные в §6.5, указывают на то, что переменная будет выбрана вместо типа данных, а тот — вместо пакета”. Это означает, что имя переменной может заслонить тип данных или пакет, а тип данных — имя пакета. С другой стороны, затенение означает, что одна переменная делает недоступной другую переменную в содержащей их области действия. Кроме того, один тип данных может затенять другой. Ни один из идентификаторов не должен заслонять или затенять другой идентификатор в содержащей их области действия. Например, в имени локальной переменной не должно пов- торно употребляться имя поля или метода из класса, а также имя класса или пакета. Анало- гично в имени внутреннего класса не должно повторно употребляться имя внешнего класса или пакета. Переопределение и затенение отличаются от сокрытия. В последнем случае доступный (как правило, незакрытый) член класса, который не должен наследоваться подклассом, за- меняется локально определенным членом подкласса, где предполагается то же самое имя, но имеется другая, несовместимая сигнатура метода. Пример кода, не соответствующего принятым нормам (затенение полей) В данном примере кода, не соответствующего принятым нормам надежного программи- рования на Java, имя поля экземпляра val повторно используется в области действия метода экземпляра.
37. Не затеняйте и не заслоняйте идентификаторы в подобластях действия 141 class MyVector { private int val = 1; private void doLogicO { int val; //... } } Получающееся в итоге поведение может быть квалифицировано как затенение. Перемен- ная метода делает переменную класса недоступной в области действия этого метода. Напри- мер, присваивание значения переменной this.val в теле метода doLogic () не оказывает никакого влияния на значение переменной класса. Решение, соответствующее принятым нормам (затенение полей) В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, затенение исключается благодаря замене с val на newValue имени переменной, определяемой в области действия метода. class MyVector { private int val = 1; private void doLogicO { int newValue; //... } Пример кода, не соответствующего принятым нормам (затенение переменных) Следующий пример кода не соответствует принятым нормам надежного программирова- ния на Java потому, что переменная i, определяемая в области действия второго цикла for, затеняет определение переменной экземпляра i, определяемой в классе MyVector. class MyVector { private int i = 0; private void doLogicO { for (i = 0; i < 10; i++) {/* ... */} for (int i = 0; i < 20; i++) {/* ... */} } Решение, соответствующее принятым нормам (затенение переменных) В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, переменная цикла i определяется только в области действия каждого цикла for. class MyVector { private void doLogic() { for (int i = 0; i < 10; i++) {/* for (int i = 0; i < 20; i++) {/* */} */}
142 Глава 3 Надежность Применимость Повторное использование имени в коде затрудняет его чтение и сопровождение, а это мо- жет ослабить защиту. Инструментальное средство автоматизированного анализа может без особого труда обнаружить повторное использование идентификаторов в содержащих их об- ластях действия. Библиография [Bloch 2005] [Bloch 2008] [Conventions 2009] [FindBugs 2008] [JLS 2013] Puzzle 67, "All Strung Out" Item 16, "Prefer Interfaces to Abstract Classes" §6.3, "Placement" DLS, "Dead store to local variable that shadows field" §6.4.1, "Shadowing" §6.4.2, "Obscuring" §7.5.2, "Type-Import-on-Demand Declarations" 38. He указывайте в одном объявлении больше одной переменной Указание нескольких переменных в одном объявлении может привести к недоразумению относительно типов переменных и их первоначальных значений. В частности, не указывайте в одном объявлении 1. Разнотипные переменные. 2. Инициализируемые переменные вместе с неинициализируемыми переменными. Как правило, каждую переменную следует объявлять в отдельной строке кода с коммен- тариями, поясняющими ее назначение. И хотя строго следовать данной рекомендации совсем не обязательно, именно такая методика программирования рекомендуется в §6.1 “Количество объявлений в одной строке кода” документа Code Conventions for the Java Programming Language (Правила оформления кода при программировании на языке Java) [Conventions 2009]. Данная рекомендация распространяется на Операторы объявления локальных переменных [JLS 2013, §14.4]. Объявления полей [JLS 2013, §8.3]. Объявления констант и полей [JLS 2013, §9.3]. Пример кода, не соответствующего принятым нормам (инициализация) Анализируя приведенный ниже пример кода, не соответствующего принятым нормам надежного программирования на Java, программирующий или просматривающий его может ошибочно подумать, что обе переменные, i и j, инициализируются значением 1. В действи- тельности инициализируется только переменная j, тогда как переменная i остается неиници- ализированной. int i, j = 1;
38. Не указывайте в одном объявлении больше одной переменной 143 Решение, соответствующее принятым нормам (инициализация) В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, вполне очевидно, что обе переменные, i и j, инициализируются зна- чением 1. int i = 1; // назначение переменной i.. . int j = 1; // назначение переменной j. . . Решение, соответствующее принятым нормам (инициализация) И в следующем решении, соответствующем принятым нормам надежного программирова- ния на Java, вполне очевидно, что обе переменные, i и j, инициализируются значением 1. int i = 1, j = 1; Каждую переменную предпочтительнее объявлять в отдельной строке кода. Тем не менее в одной строке кода допускается также объявление нескольких переменных, когда они являют- ся обыкновенными временными переменными вроде индексов массивов. Пример кода, не соответствующего принятым нормам (разные типы данных) В данном примере кода, не соответствующего принятым нормам надежного программиро- вания на Java, несколько переменных, включая и массив, объявляются в одной строке. У всех экземпляров типа Т имеется доступ к методам класса Object. Но в то же время можно легко забыть, что массивы требуют особого обращения при переопределении некоторых из этих методов. public class Example<T> { private Ta, b, c[], d; public Example (T in) { a = in; b = in; c = (T[]) new Object [10]; d = in; } } При переопределении метода из класса Object, например метода toString (), можно не- умышленно предоставить реализацию этого метода для экземпляра типа Т без учета того, что с — это массив объектов типа Т, а не ссылка на объект типа Т, как показано ниже. public String toString() { return a.toString() + b.toString() + c.toString() + d.toString(); } Но ведь в данном случае метод toString (), скорее всего, предполагается вызвать для каждого отдельного элемента массива с. // правильная функциональная реализация public String toString() {
144 Глава 3 Надежность String s = a.toStringO + b.toStringO ; for (int i = 0; i < c.length; i++) { s += c[i].toString(); } s += d.toString(); return s; Решение, соответствующее принятым нормам (разные типы данных) В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, каждое объявление размещается в отдельной строке, а для объявле- ния массива используется наиболее предпочтительная запись. public class Example<T> { private Та; // назначение переменной а. . . private Т b; // назначение переменной Ь. . . private Т[] с; // назначение массива с[] . . . private Т d; // назначение переменной d.. . public Example(Т in) { а = in; b = in; c= (T[]) new Object [10] ; d = in; } } Применимость Вследствие объявления нескольких переменных в одной строке кода снижается удобочи- таемость кода и в связи с этим у программистов возникают недоразумения. Если все-таки требуется объявить несколько переменных в одной строке кода, то следует обеспечить само- очевидность типа и исходного значения каждой переменной. Объявления переменных цикла for должны быть включены в его оператор — даже в ущерб комментариям, поясняющим назначение переменных в их объявлениях, как показано ниже. Такие объявления совсем не обязательно делать в отдельных строках кода, а поясняющие комментарии можно опустить. public class Example { void function() { int mx = 100; // некоторое максимальное значение for (int i = 0; i < mx; ++i ) { Библиография [Conventions 2009] [ESA 2005] [JLS 2013] §6.1, "Number Per Line" Rule 9, Put Single Variable Definitions in Separate Lines §4.3.2, "The class Object"
39. Пользуйтесь описательными символическими константами... 145 §6.1, "Declarations" §8.3, "Field Declarations" §9.3, "Field (Constant) Declarations" §14.4, "Local Variable Declaration Statements" 39. Пользуйтесь описательными символическими константами для обозначения литеральных значений в логике программы В Java поддерживается применение разнотипных литералов, в том числе целых чисел (5, 2), чисел с плавающей точкой (2.5, 6.022е+23), символов (’ а ’, ’ \п ’), логических зна- чений (true, false), а также символьных строк ("HelloXn"). Обширное применение лите- ралов в программе может вызвать два затруднения. Во-первых, смысловое значение литерала нередко заслонено или неясно из контекста. И во-вторых, для изменения часто используе- мого литерала его приходится искать в исходном коде программы, отличая те примеры его применения, где он должен быть видоизменен, от тех примеров, где он должен оставаться без изменения. Во избежание упомянутых выше затруднений переменные класса следует объявлять с описательными именами констант, устанавливая требуемые литералы в качестве их значений и обращаясь к константам вместо литералов повсюду в программе. Такой подход ясно пока- зывает смысловое значение или предполагаемое применение каждого литерала. Более того, если требуется изменить константу, такое изменение ограничивается объявлением, избавляя от необходимости искать ее в остальной части кода. Константы должны объявляться как статические и конечные (static final), как пока- зано в приведенном ниже примере. Но они не должны объявляться открытыми и конечными (public final), если их значения могут измениться (см. рекомендацию 31 “Не объявляйте открытыми и конечными константы, значения которых могут измениться в последующих вы- пусках программы”). private static final int SIZE = 25; Несмотря на то что с помощью модификатора final можно указывать неизменяемые кон- станты, это правило не распространяется на составные объекты. Подробнее об этом см. в ре- комендации 73 “Не путайте неизменяемость ссылки и доступного по ссылке объекта”. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программиро- вания на Java, рассчитываются приблизительные размеры сферы по заданному радиусу. double area(double radius) { return 3.14 * radius * radius; } double volume(double radius) { return 4.19 * radius * radius * radius;
146 Глава 3 Надежность double greatCircleCircumference(double radius) { return 6.28 * radius; } В приведенных выше методах произвольные литералы 3.14, 4.19 и 6.28, по-видимому, используются для обозначения различных масштабных коэффициентов, применяемых для расчета размеров сферы. Читая рассматриваемый здесь исходный код, разработчик или тот, кто его сопровождает, едва ли сможет догадаться, каким образом эти литералы были сфор- мированы и что именно они означают, а следовательно, он не поймет истинное назначение данного кода. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программи- рования на Java, предпринимается попытка устранить упомянутый выше недостаток путем явного расчета требуемых констант. double area(double radius) { return 3.14 * radius * radius; } double volume(double radius) { return 4.0 / 3.0 * 3.14 * radius * radius * radius; } double greatCircleCircumference(double radius) { return 2 * 3.14 * radius; } В приведенном выше коде для обозначения числа п используется литерал 3.14. И хотя такой подход отчасти устраняет неоднозначность литералов, тем не менее он усложняет со- провождение кода. Так, если программист решит, что для расчета потребуется более точное значение числа п, ему придется найти и заменить все вхождения литерала 3.14 в исходном коде. Решение, соответствующее принятым нормам (константы) В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, константа PI объявляется и инициализируется значением 3.14. А в остальной части кода происходит обращение к этой константе всякий раз, когда требуется значение числа п. private static final double PI = 3.14; double area(double radius) { return PI * radius * radius; } double volume(double radius) { return 4.0/3.0 * PI * radius * radius * radius; } double greatCircleCircumference(double radius) { return 2 * PI * radius; }
39. Пользуйтесь описательными символическими константами... 147 Данное решение проясняет исходный код и способствует его сопровождению. Так, если потребуется более точная аппроксимация значения числа п, то для этого достаточно пере- определить константу PI. А применение литералов 4.0, 3.0 и 2 не противоречит данной рекомендации по причинам, поясняемым далее в разделе “Применимость”. Решение, соответствующее принятым нормам (предопределенные константы) Если доступны предопределенные константы, то ими следует пользоваться в первую оче- редь. Так, в классе java.lang.Math определен целый ряд числовых констант, включая PI и экспоненциальную константу Е. Ниже показано применение константы PI непосредственно в коде. double area(double radius) { return Math.PI * radius * radius; } double volume(double radius) { return 4.0/3.0 * Math.PI * radius * radius * radius; } double greatCircleCircumference(double radius) { return 2 * Math.PI * radius; } Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программиро- вания на Java, сначала определяется константа BUFSIZE, а затем ее определение как констан- ты отменяется допущением конкретного значения размера буфера (BUFSIZE) в приведенном ниже выражении. private static final int BUFSIZE = 512; // ... public void shiftBlockO { int nblocks = 1 + ((nbytes - 1) » 9) ; // BUFSIZE = 512 = 2A9 // ... } В данном примере кода предполагается, что значение константы BUFSIZE равно 512, а сдвиг вправо на 9 битов равнозначен делению (положительных чисел) на 512. Но, если зна- чение константы BUFSIZE впоследствии изменится на 1024, в коде произойдут изменения, чреватые ошибками, которые нелегко обнаружить. Кроме того, рассматриваемый здесь код не соответствует рекомендации “NUM01-J. Не выполняйте поразрядные и арифметические операции над одними и теми же данными” из стандарта The CERT* Oracle* Secure Coding Standard for Java"* [Long 2012]. Замена операции де- ления на сдвиг вправо считается преждевременной оптимизацией. Как правило, выполнение такой оптимизации лучше поручить компилятору, который найдет для нее более подходящий момент.
148 Глава 3 Надежность Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, используется идентификатор, присваиваемый значению константы в выражении. private static final int BUFSIZE = 512; // ... public void shiftBlock(int nbytes) { int nblocks = 1 + (nbytes - 1) / BUFSIZE; // ... } Применимость Применение числовых литералов затрудняет чтение, понимание и редактирование исход- ного кода. А применение символических констант должно быть ограничено теми случаями, в которых они повышают удобочитаемость и сопровождаемость исходного кода. Если назначе- ние литерала очевидно или его конкретное значение вряд ли изменится, то применение сим- волических констант способно лишь ухудшить удобочитаемость исходного кода. В приведен- ном ниже примере смысловое значение кода заслоняется из-за того, что в нем используется слишком много символических констант. private static final double FOUR = 4.0; private static final double THREE = 3.0; double volume(double radius) { return FOUR / THREE * Math.PI * radius * radius * radius; } Значения 4.0 и 3.0 явно обозначают масштабные коэффициенты в расчете объема сферы и вряд ли изменятся, в отличие от значения числа п, а следовательно, они могут быть указаны в исходном коде непосредственно. И нет никаких причин повышать их точность, а их замена на символические константы только ухудшит удобочитаемость исходного кода. Библиография [Core Java 2003] [Long 2012] NUM01-J. Do not perform bitwise and arithmetic operations on the same data 40. Правильно кодируйте отношения в определениях констант Определения констант в выражениях должны быть точно соотнесены, если тесно связаны значения, которые они выражают.
41. Возвращайте из методов пустой массив или коллекцию вместо пустого значения 149 Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программи- рования на Java, константа OUT_STR_LEN должна всегда быть точно в два раза больше конс- танты IN_STR_LEN. Но приведенные ниже определения этих констант не отвечают данному требованию. public static final int IN_STR_LEN = 18; public static final int OUT_STR_LEN = 20; Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, отношение между двумя значениями констант представлено в их оп- ределениях. public static final int IN_STR_LEN = 18; public static final int OUT_STR_LEN = IN_STR_LEN + 2; Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программиро- вания на Java, отношение между двумя константами является только кажущимся, а на самом деле оно не существует. public static final int VOTING_AGE = 18; public static final int ALCOHOL_AGE = VOTING_AGE + 3; Программист, выполняющий регулярное сопровождение этого кода, может видоизменить определение константы VOTING AGE, не обратив внимание на то, что это повлечет за собой изменение и в определении константы ALCOHOL_AGE. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, определения констант отражают их независимость друг от друга. public static final int VOTING_AGE = 18; public static final int ALCOHOL_AGE =21; Библиография [JLS 2013] §4.12.4, "final Variables" 41. Возвращайте из методов пустой массив или коллекцию вместо пустого значения В некоторых прикладных интерфейсах API из методов намеренно возвращается пустая ссылка, указывающая на то, что экземпляры недоступны. Такая практика программиро- вания может привести к уязвимостям типа отказа в обслуживании, когда клиентский код
150 Глава 3 Надежность оказывается не в состоянии обработать возвращаемое пустое значение. А ведь пустое значе- ние служит характерным примером внутреннего индикатора ошибки, проявление которого нежелательно в соответствии с рекомендацией 52 “Избегайте внутренних индикаторов оши- бок” В тех методах, где ряд значений возвращается в виде массива или коллекции, отличной альтернативой возврату пустого значения является возврат пустого массива или коллекции, поскольку вызывающий код, как правило, лучше оснащен для обработки пустого множества, чем пустого значения. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программиро- вания на Java, возвращается пустой списочный массив типа ArrayList, если его размер ра- вен нулю. В классе Inventory содержится метод getStock (), составляющий список товаров с нулевыми запасами и возвращающий этот список вызывающему коду. class Inventory { private final Hashtable<String, Integer> items; public Inventory() { items = new Hashtable<String, Integer>(); } public List<String> getStock() { List<String> stock = new ArrayList<String>(); Enumeration itemKeys = items.keys(); while (itemKeys.hasMoreElements()) { Object value = itemKeys.nextElement(); if ((items.get(value)) == 0) { stock.add((String)value); } } if (items.size () == 0) { return null; } else { return stock; } } } public class Client { public static void main(String[] args) { Inventory inv = new Inventory(); List<String> items = inv.getStock(); System.out.printin(items.size()); } } Когда размер этого списка оказывается равным нулю, возвращается пустое значение, ис- ходя из предположения, что клиент установит все необходимые проверки. Но в данном при- мере кода клиенту как раз и не хватает нужной проверки пустого значения, что в конечном итоге приводит к исключению типа NullPointerException во время выполнения.
41. Возвращайте из методов пустой массив или коллекцию вместо пустого значения 151 Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, вместо пустого значения возвращается список типа List, даже если он и пустой. class Inventory { private final Hashtable<String, Integer> items; public Inventory() { items = new Hashtable<String, Integer>(); } public List<String> getStock() { List<String> stock = new ArrayList<String>(); Integer noOfItems; // количество товаров, оставшихся в запасах Enumeration itemKeys = items.keys(); while (itemKeys.hasMoreElements ()) { Object value = itemKeys.nextElement (); if ((noOfltems = items.get(value)) == 0) { stock.add((String)value); } } return stock; // возвратить список (возможно, и нулевой длины) } } public class Client { public static void main(String[] args) { Inventory inv = new Inventory(); List<String> items = inv.getStock(); System.out.printin(items.size()); } } В данном решении клиент может эффективно обработать возвращаемый ему резуль- тат, не прерывая выполнение кода исключениями. Если вместо коллекций возвращаются массивы, то следует исключить попытки клиента получить доступ к отдельным элементам массива нулевой длины, а следовательно, и условий для возникновения исключения типа ArraylndexOutOfBoundsException. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, возвращается явно пустой список. И это вполне соответствует допус- тимой методике программирования. public List<String> getStock() { List<String> stock = new ArrayList<String>(); Integer noOfltems; // количество товаров, оставшихся в запасах Enumeration itemKeys = items.keys(); while (itemKeys.hasMoreElements()) { Object value = itemKeys.nextElement(); if ((noOfltems = iterns.get(value)) == 0) { stock.add((String)value);
152 Глава 3 Надежность } } if (l.isEmpty() ) { return Collections.EMPTY_LIST; // этот список всегда нулевой длины } else { return stock; // возвратить список } } // Класс Client . . . Применимость Если из метода возвращается пустое значение, а не массив или коллекция нулевой дли- ны, то в конечном итоге это может привести к уязвимостям типа отказа в обслуживании, когда клиентский код окажется не в состоянии правильно обработать возвращаемые пустые значения. Обнаружение пустого значения происходит автоматически, а для устранения этого недостатка, как правило, требуется вмешательство программиста. Библиография [Bloch 2008] Item 43, "Return Empty Arrays or Collections, Not Nulls" 42. Пользуйтесь исключениями только в особых случаях Исключения должны использоваться только в особых случаях, обозначая исключитель- ные условия. Их не следует использовать для обычного управления ходом выполнения про- граммы. Перехватывая обобщенный объект, например типа Throwable, вряд ли можно пе- рехватить непредвиденные ошибки. Характерные тому примеры приведены в рекомендации “ERR08-J. Не перехватывайте исключение типа NullPointerException и любые его пред- шественники” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012]. Когда в программе перехватывается конкретный тип исключения, то не всегда известно, где именно это исключение было сгенерировано. Обработка в блоке оператора catch исключе- ния, возникающего в известном отдаленном месте, считается неудачным решением. Поэто- му ошибку предпочтительнее обработать, как только она возникнет, а если возможно, то и предотвратить ее. Нелокальность операторов throw и сопутствующих им операторов catch мешает также оптимизаторам усовершенствовать код, опирающийся на обработку исключений. Кроме того, расчет на перехват исключений для управления ходом выполнения программы усложняет ее отладку, поскольку исключения обозначают передачу управления от оператора throw к опе- ратору catch. И наконец, исключения совсем не обязательно должны быть тщательно опти- мизированы, если предполагается, что они должны генерироваться только в исключительных ситуациях. Генерирование и перехват исключений нередко ухудшает производительность в большей степени, чем обработка ошибок каким-нибудь другим механизмом.
42. Пользуйтесь исключениями только в особых случаях 153 Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программи- рования на Java, предпринимается попытка сцепления обрабатываемых элементов из массива символьных строк. public String processSingleString(String string) { // ... return string; } public String processstrings(String[] strings) { String result = int i = 0; try { while (true) { result = result.concat(processSingleString(strings[i])); } } catch (ArraylndexOutOfBoundsException e) { // игнорировать, т.к. обработка завершена } return result; } В приведенном выше примере кода исключение ArraylndexOutOfBoundsException используется для обнаружения конца массива. К сожалению, это исключение отно- сится к типу RuntimeException, и поэтому оно может быть сгенерировано методом processSingleString () без объявления в операторе throws. Следовательно, вполне воз- можно, что выполнение метода processSingleString () завершится прежде, чем будут об- работаны все символьные строки. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, для сцепления символьных строк организуется стандартный цикл for. public String processstrings(String[] strings) { String result = for (int i = 0; i < strings.length; i++) { result = result.concat(processSingleString(strings[i])); } return result; } Перехватывать исключение ArraylndexOutOfBoundsException в приведенном ниже коде не нужно, потому что оно возникает во время выполнения. Подобные исключения ука- зывают на те ошибки, которые лучше всего исправляются путем устранения дефекта в самом коде.
154 Глава 3 Надежность Применимость Применение исключений для иных целей, кроме обнаружения и обработки исключитель- ных ситуаций, усложняет анализ и отладку программы, снижает производительность и уве- личивает затраты на сопровождение программы. Библиография [Bloch 2001] Item 39, "Use Exceptions Only for Exceptional Conditions" [JLS 2013] Chapter 11, "Exceptions" [Long 2012] ERR08-J. Do not catch NullPointerException or any of its ancestors 43. Пользуйтесь оператором try с ресурсами для безопасного обращения с закрываемыми ресурсами В наборе инструментальных средств Java Development Kit 1.7 (JDK 1.7) внедрен оператор try с ресурсами (см. JLS, §14.20.3 “Оператор try с ресурсами” спецификации JLS [JLS 2013]). Этот оператор упрощает правильное использование ресурсов, реализующих интерфейсы j ava.lang.AutoCloseable и j ava.io.Closeable. Применение оператора try с ресурсами предотвращает осложнения, которые могут воз- никнуть в обыкновенном блоке try-catch-finally. К их числу относится неспособность закрыть ресурс из-за того, что в результате закрытия другого ресурса генерируется исключе- ние, или же маскирование важного исключения при закрытии ресурса. Применение оператора try с ресурсами иллюстрируется также в рекомендациях “ERR05-J. Не допускайте исчезновения проверяемых исключений из блока finally”, “FIO03-J. Удаляй- те временные файлы перед завершением кода”, а также “FIO04-J. Закрывайте ресурсы, если они больше не нужны” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012]. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного програм- мирования на Java, при попытке закрыть два ресурса применяется обыкновенный блок try-catch-finally. Но если генерируется исключение при закрытии буферизованно- го потока чтения br типа BufferedReader, то буферизованный поток записи bw типа Buf feredWriter не будет закрыт. public void processFile(String inPath, String outPath) throws lOException{ BufferedReader br = null; BufferedWriter bw = null; try { br = new BufferedReader(new FileReader(inPath)); bw = new BufferedWriter(new FileWriter(outPath)); // обработать вводимые данные и вывести результат } finally { try { if (br != null) { br .close () ;
43. Пользуйтесь оператором try с ресурсами для безопасного обращения... 155 } if (bw != null) { bw.close(); } } catch (lOException x) { // обработать ошибку Решение, соответствующее принятым нормам (второй блок finally) В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, второй блок finally применяется для гарантии того, что ресурс bw будет закрыт должным образом даже в том случае, если при закрытии ресурса br генериру- ется исключение. public void processFile(String inPath, String outPath) throws lOException { BufferedReader br = null; BufferedWriter bw = null; try { br = new BufferedReader(new FileReader(inPath)); bw = new BufferedWriter(new FileWriter(outPath)); // обработать вводимые данные и вывести результат } finally { if (br != null) { try { br .close () ; } catch (lOException x) { // обработать ошибку } finally { if (bw != null) { try { bw.close (); } catch (lOException x) { // обработать ошибку } } } } } } Решение, соответствующее принятым нормам (оператор try с ресурсами) В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, оператор try с ресурсами применяется для управления обоими ре- сурсами, br и bw. public void processFile (String inPath, String outPath) throws lOException{
156 Глава 3 Надежность try (BufferedReader br = new BufferedReader(new FileReader(inPath)); BufferedWriter bw = new BufferedWriter(new FileWriter(outPath));) { // обработать вводимые данные и вывести результат } catch (lOException ex) { // вывести все исключения, включая и подавляемые System.err.printin("thrown exception: " + ex.toString()); Throwable[] suppressed = ex.getSuppressed(); for (int i = 0; i < suppressed.length; i++) { System.err.printin("suppressed exception: " + suppressed[i].toString()); } } } В данном решении сохраняются любые исключения, генерируемые во время обработки вводимых данных, и в то же время гарантируется надлежащее закрытие обоих ресурсов, br и bw, независимо от типа генерируемых исключений. И наконец, в рассматриваемом здесь коде демонстрируется, каким образом осуществляется доступ к каждому исключению, которое мо- жет быть сгенерировано в блоке оператора try с ресурсами. Если только исключение не генерируется, то во время открытия, обработки или закрытия файлов само исключение будет выведено после сообщения "thrown exception:" (сгене- рировано исключение). Если же одно исключение генерируется во время обработки, а дру- гое — при попытке закрыть любой из файлов, то второе исключение будет выведено после сообщения "thrown exception: ", а первое исключение — после сообщения "suppressed exception:" (подавленное исключение). Применимость Если не обработать правильно все сбойные ситуации во время работы с закрываемыми ресурсами, то некоторые ресурсы могут остаться незакрытыми или же замаскированными важные ресурсы, а в конечном итоге это может привести к отказу в обслуживании. Следует также иметь в виду, что неприменение оператора try с ресурсами само по себе нельзя считать уязвимым в отношении безопасности, поскольку имеется возможность написать правильно структурированную группу вложенных блоков операторов try-catch-f inally для защиты используемых ресурсов (см. рекомендацию “ERR05-J. Не допускайте исчезновение проверяе- мых исключений из блока finally” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012]). Это означает, что отсутствие правильной обработки подобных исключи- тельных и ошибочных ситуаций обычно служит источником уязвимостей в защите. Данное затруднение отчасти устраняется с помощью оператора try с ресурсами, поскольку он гаран- тирует правильное управление ресурсами, а также полностью предотвращает маскирование исключений. Библиография [JLS 2013] §14.20.3, "try-with-resources" [Long 2012] ERR05-J. Do not let checked exceptions escape from a finally block FIO03-J. Remove temporary files before termination FIO04-J. Close resources when they are no longer needed [Tutorials 2013] The try-with-resources Statement
44. Не пользуйтесь утверждениями для проверки отсутствия ошибок... 157 44. Не пользуйтесь утверждениями для проверки отсутствия ошибок при выполнении Диагностические тесты могут быть внедрены в программы с помощью оператора утверж- дения assert. Утверждения предназначены главным образом для применения во время от- ладки и зачастую отключаются перед развертыванием кода с помощью параметра команд- ной строки -disableassertions (или сокращенно -da) во время выполнения программ на Java. Следовательно, утверждения следует применять для защиты от неверных допущений программирующих на Java, а не для проверки ошибок при выполнении кода. Утверждения вообще не следует использовать для проверки отсутствия ошибок при вы- полнении кода, в том числе в случаях, описанных ниже. Недостоверный ввод данных пользователем, включая аргументы командной строки и переменные окружения. Ошибки обращения к файлам (например, ошибки открытия, чтения или записи в файлы). Сетевые ошибки, включая ошибки в сетевых протоколах. Условия исчерпания оперативной памяти, когда виртуальная машина JVM не в состо- янии выделить память для нового объекта, а система “сборки мусора” — освободить достаточно места в оперативной памяти. Исчерпание системных ресурсов (например, дескрипторы выхода за пределы файла, процессы и потоки). Ошибки системных вызовов (например, ошибки выполнения файлов, блокировки или разблокировки мьютексов). Недостоверные полномочия (например, на доступ к файлам, оперативной памяти или предоставляемые пользователям). Код, защищающий, например, от ошибок ввода-вывода, не может быть реализован как утверждение, поскольку он должен присутствовать в развертываемом исполняемом файле. Как правило, утверждения для развертывания непригодны для серверных программ или встраиваемых систем. Неудачное утверждение может привести к атакам типа отказа в обслу- живании, если они инициируются злонамеренным пользователем. В подобных случаях более подходящим оказывается режим потенциальной неисправности, например, для записи в файл регистрации. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программиро- вания на Java, оператор assert применяется для проверки достоверности вводимых данных. BufferedReader br; / / установить буферизованный поток чтения br типа BufferedReader String line; // ...
158 Глава 3 Надежность line = br.readLine(); assert line != null; Вследствие того что доступность ввода данных зависит от пользователя и может быть ис- черпана в любой момент во время выполнения программы, надежная программа должна быть готова к изящной обработке ошибок и восстановлению доступности ввода данных. Но при- менение оператора assert для проверки достоверности значительного хотя бы в какой-то степени объема вводимых данных не годится потому, что это может привести к внезапному завершению процесса, а в конечном итоге и к отказу в обслуживании. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, демонстрируется рекомендуемый способ обращения с недоступнос- тью вводимых данных. BufferedReader br; // установить буферизованный поток чтения br типа BufferedReader String line; // ... line = br.readLine(); if (line == null) { // обработать ошибку } Применимость Утверждения являются ценным диагностическим средством для поиска и устранения в программном обеспечении неисправностей, которые могут привести к уязвимостям в его за- щите. Но отсутствие утверждений совсем не означает, что код свободен от программных оши- бок. Как правило, злоупотребление операторами assert для проверки ошибок при выполне- нии кода вместо проверки логических ошибок в нем нельзя обнаружить автоматически. Библиография [JLS 2013] §14.10, "The assert Statement" 45. Пользуйтесь вторым и третьим однотипными операндами в условных выражениях В качестве первого операнда условного оператора ?: указывается логическое значение, чтобы выяснить, какой из двух других операндов следует вычислять (см. §15.25 “Условный оператор ?:” в спецификации JLS [JLS 2013]). Общая форма условного выражения с операто- ром ?: такова: operandl ? operand2 : operands
45. Пользуйтесь вторым и третьим однотипными операндами... 159 Оператор ?: действует следующим образом. Если первый операнд (operandl) принимает логическое значение true, то выбирает- ся выражение во втором операнде (operand2). А если первый операнд принимает логическое значение false, то выбирается выраже- ние в третьем операнде (operand3). Условный оператор синтаксически является правоассоциативным. Например, условные выражения a?b:c?d:e?f :д и a?b: (c?d: (e?f :д) ) равнозначны. Правила, установленные в спецификации JLS, для определения типа результата условного выражения довольно сложны (табл. 3.1). Поэтому и не удивительно, что программирующие на Java иногда затрудняются оп- ределить тип преобразований, требующихся для условных выражений, которые они пишут. Таблица 3.1. Определение результирующего типа условного выражения Правило Операнд 2 Операнд 3 Результирующий тип 1 Тип Т Тип Т Тип Т 2 Логическое значение Логическое значение Логическое значение 3 Логическое значение Логическое значение Логическое значение 4 Пустое значение Ссылка Ссылка 5 Ссылка Пустое значение Ссылка б byte или Byte short или Short short 7 short или Short byte или Byte short 8 byte, short, char, Byte, Short, Character Константа типа int byte, short, char, если может быть представлено значением типа int 9 Константа типа int byte, short, char, Byte, Short, Character byte, short, char, если может быть представлено значением типа int 10 Другой числовой тип Другой числовой тип Продвигаемый тип второго или третьего операнда 11 Т1 = упаковывающее преобразование (S1) T2 = упаковывающее преобразование (S2) Выполнить фиксирующее преобра- зование наименьшей из верхних границ типов Т1 и Т2 Определение типа результата условного выражения начинается с верхней части табли- цы. Сначала компилятор применяет первое совпадающее правило. В столбцах “Операнд 2” и “Операнд 3м данной таблицы указаны операнды operand2 и operands условного операто- ра ?: соответственно. А константа типа int обозначает константные выражения типа int (например, 10 ’ или переменные, объявляемые как конечные — final). В последней строке таблицы S1 и S2 обозначают типы второго и третьего операндов со- ответственно, Т1 — тип, получающийся в результате упаковывающего преобразования опе- ранда SI, Т2 — тип, получающийся в результате упаковывающего преобразования операнда S2, а тип всего условного выражения получается в результате фиксирующего преобразования наименьшей из верхних границ типов Т1 и Т2. Подробнее об этом см. §5.1.7 “Упаковывающее преобразование”, §5.1.10 “Фиксирующее преобразование” и §15.12.2.7 “Выведение типов аргу- ментов из конкретных аргументов” в спецификации JLS [JLS 2013].
160 Глава 3 Надежность Сложность правил, определяющих результирующий тип условного выражения, может стать причиной непреднамеренных преобразований типов. Следовательно, второй и третий операнды каждого условного выражения должны иметь одинаковые типы данных. Данная ре- комендация распространяется и на упакованные примитивные типы данных. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программи- рования на Java, предполагается, что оба оператора будут выводить значение типа char пе- ременной alpha. public class Expr { public static void main(String[] args) { char alpha = ' A'; int i = 0; // другой код, где значение переменной i может измениться boolean trueExp = true; // результатом вычисления этого выражения // является логическое значение true System.out .print (trueExp ? alpha : 0); // выводит символа System.out.print(trueExp ? alpha : i); // выводит значение 65 } } В приведенном выше коде сначала выводится символ А, поскольку компилятор применяет правило 8 из таблицы определения результирующего типа условного выражения (см. табл. 3.1), чтобы выяснить, относятся ли второй и третий операнды к типу char или они уже преобра- зованы к этому типу. Но во втором операторе print значение символа А уже выводится как относящееся к типу int. Первым совпадающим правилом из данной таблицы оказывается правило 10. Следовательно, компилятор продвигает значение символа А к типу int. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, применяются одинаковые типы второго и третьего операндов каж- дого условного выражения. А благодаря явному приведению типов указывается предполага- емый тип данных. public class Expr { public static void main(String[] args) { char alpha = 'A*; int i = 0; boolean trueExp = true; // результатом вычисления этого выражения // является логическое значение true System.out .print (trueExp ? alpha : 0); // выводит символа // намеренно суженное приведение типа переменной i; // возможное усечение вполне допустимо System.out .print (trueExp ? alpha : ((char) i) ); // выводит символа } } Когда значение переменной i во втором условном выражении выходит за пределы диапа- зона представления значений типа char, значение этой переменной усекается в результате яв- ного приведения типов. Такой прием согласуется с исключением NUM12-EX0 из рекомендации
45. Пользуйтесь вторым и третьим однотипными операндами... 161 “NUM12-J. Предотвращайте потери или неверную интерпретацию данных при преобразова- ниях числовых типов, приводящих к их сужению” в стандарте The CERT9 Oracle9 Secure Coding Standard for Java" [Long 2012]. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программиро- вания на Java, вместо предполагаемого результата (в пределах от 0 до 50) выводится числовое значение 100 размера хеш-множества типа HashSet. public class ShortSet { public static void main(String[] args) { HashSet<Short> s = new HashSet<Short>(); for (short i = 0; i < 100; i++) { s.add(i); // приведение типов в выражении i-1 безопасно, // т.к. значение всегда представимо Short workingVal = (short) (i-1); // ... переменная workingVal может быть обновлена в другом коде s.remove(((i % 2) == 1) ? i-1 : workingVal); } System.out.println(s.size()); } } Сочетание типов short и int значений во втором операнде (i-1) условного выражения приводит к результирующему типу int в соответствии с правилами продвижения целочис- ленных типов. Следовательно, объект типа Short в третьем операнде условного выражения распаковывается в тип short, а затем продвигается к типу int. Далее результат вычисле- ния условного выражения автоматически упаковывается в объект типа Integer. А посколь- ку хеш-множество типа HashSet содержит только объекты типа Short, то вызов метода HashSet. remove () практически ничего не дает. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, второй операнд условного выражения сначала приводится к типу short, а затем явно вызывается метод Short .valueOf () для получения экземпляра типа Short, значение которого равно i-1. public class ShortSet { public static void main(String[] args) { HashSet<Short> s = new HashSet<Short>(); for (short i = 0; i < 100; i++) { s.add(i); // приведение типов в выражении i-1 безопасно, // т.к. значение всегда представимо Short workingVal = (short) (i-1); // ... переменная workingVal может быть обновлена в другом коде // приведение типов в выражении i-1 безопасно, // т.к. значение всегда представимо s.remove(((i % 2) == 1) ? Short .valueOf ((short) (i-1)) : workingVal);
162 Глава 3 Надежность } System.out.printin(s.size()); В результате приведения типов второй и третий операнды условного выражения относят- ся к типу Short, а вызов метода remove () дает ожидаемый результат. Следующее написание условного выражения: ((i % 2) == 1) ? (short) (i—1) ) : workingVal также согласуется с данной рекомендацией, поскольку в такой его форме второй и третий операнды относятся к типу short. Тем не менее данное альтернативное решение оказыва- ется менее эффективным, поскольку оно приводит к принудительной распаковке значения переменной workingVal и автоматической упаковке (из типа short в тип Short) результата вычисления условного выражения на каждом шаге цикла. Применимость Когда второй и третий операнды условного выражения относятся к разным типам, они могут быть подвержены неожиданным преобразованиям типов. Автоматическое обнаруже- ние условных выражений, второй и третий операнды которых относятся к разным типам, осуществляется напрямую. Библиография [Bloch 2005] [Findbugs 2008] [JLS 2013] [Long 2012] Puzzle 8, "Dos Equis" "Bx: Primitive Value Is Unboxed and Coerced for Ternary Operator" §15.25, "Conditional Operator ?:" NUM12-J. Ensure conversions of numeric types to narrower types do not result in lost or misinterpreted data 46. Не выполняйте сериализацию прямых описателей системных ресурсов Сериализованные объекты могут быть изменены вне любой программы на Java, если толь- ко они не защищены с помощью таких механизмов, как герметизация и подписание (см. ре- комендацию “ENVOI-J. Размещайте весь уязвимый для безопасности код в одном архивном JAR-файле, подписывая и герметизируя его” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]). Если объект, обращающийся к системным ресурсам, стано- вится сериализованным, а совершающий атаку злоумышленник может изменить сериализо- ванную форму этого объекта, то появляется возможность видоизменить системный ресурс, на который ссылается сериализованный объект как его описатель. Например, совершающий атаку злоумышленник может видоизменить сериализованный описатель файла, чтобы обра- титься к произвольному файлу в системе. В отсутствие диспетчера защиты любые операции, в которых применяется такой описатель файла, могут быть выполнены по пути к файлу и имени файла, предоставляемым совершающим атаку злоумышленником.
46. Не выполняйте сериализацию прямых описателей системных ресурсов 163 Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программиро- вания на Java, сериализованный объект типа File объявляется в классе Ser. final class Ser implements Serializable { File f; public Ser() throws FileNotFoundException { f = new File ("c: WfilepathWfilename") ; } } Сериализованная форма объекта раскрывает путь к файлу, который может быть изменен. Когда объект десериализован, операции выполняются по измененному пути, что может при- вести к чтению или видоизменению не того файла. Решение, соответствующее принятым нормам (без реализации класса Serializable) В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, демонстрируется конечный класс Ser, в котором не реализуется класс java. io. Serializable. Следовательно, объект типа File не может быть сериализован. final class Ser { File f; public Ser() throws FileNotFoundException { f = new File ("c: WfilepathWfilename"); } } Решение, соответствующее принятым нормам (объявление объекта переходным) В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, объект типа File объявляется как transient. В итоге путь к файлу не сериализуется в остальной части класса, а следовательно, он не раскрывается для соверша- ющих атаки злоумышленников. final class Ser implements Serializable { transient File f; public Ser() throws FileNotFoundException { f = new File ("c: WfilepathWfilename") ; } } Применимость Десериализация прямых описателей системных ресурсов может разрешить видоизменение ресурсов, к которым происходит обращение. Библиография [Long 2012] ENVOI-J. Place all security-sensitive code in a single JAR and sign and seal it [Oracle 2013c] Java Platform Standard Edition 7 Documentation
164 Глава 3 Надежность 47. Отдавайте предпочтение итераторам над перечислениями В документации на интерфейс Enumeration<E>, входящий в состав прикладного интер- фейса Java API [API 2013], говорится следующее: “Объект из класса, реализующего интерфейс Enumeration, формирует по очере- ди последовательный ряд элементов. В результате последовательных вызовов метода nextElement () возвращаются элементы из этого последовательного ряда”. В качестве примера ниже приведен фрагмент кода, в котором интерфейс Enumeration используется для отображения содержимого объекта типа Vector. for (Enumeration е = vector.elements(); e.hasMoreElements();) { System.out.printIn(e.nextElement()); } В документации на прикладной интерфейс Java API [API 2013] рекомендуется следующее: “В новых реализациях следует отдавать предпочтение интерфейсу Iterator над интерфей- сом Enumeration”. Итераторы предпочтительнее перечислений, потому что в них использу- ются более простые имена методов, и, в отличие от перечислений, итераторы обладают впол- не определенной семантикой для удаления элементов из коллекции при ее последовательном обходе. Таким образом, итераторам следует отдавать предпочтение над перечислениями при обращении к итерируемым коллекциям. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программи- рования на Java, реализуется класс Bankoperations, где метод removeAccounts () служит для прекращения срока действия всех счетов конкретного их владельца, распознаваемого по имени. В методе remove () предпринимается попытка обойти по очереди все векторные за- писи, сравнивая каждую из них с именем ’’Harry”. class Bankoperations { private static void removeAccounts(Vector v, String name) { Enumeration e = v.elements(); while (e.hasMoreElements()) { String s = (String) e.nextElement(); if (s.equals(name)) { v.remove(name); // второе имя Harry не удаляется } } // отобразить текущих владельцев счетов System.out.printIn("The names are:"); e = v.elements(); while (e.hasMoreElements ()) { // вывести имена Dick, Harry, Tom System.out.printin(e.nextElement()); } }
47. Отдавайте предпочтение итераторам над перечислениями 165 public static void main (String args[]) { // список содержит отсортированный массив имен владельцев счетов, // повторение которых допускается List list = new ArrayList(Arrays.asList( new String[] {"Dick", "Harry”, "Harry", "Tom"})); Vector v = new Vector(list); removeAccount (v, "Harry"); } } Как только встречается первое имя "Harry", его запись успешно удаляется, а размер век- тора уменьшается до трех. Но индекс перечисления типа Enumeration остается без измене- ния, в результате чего в программе выполняется следующее (теперь уже окончательное) срав- нение с именем "Тот". Следовательно, второе имя "Harry" остается невредимым, сдвигаясь на вторую позицию в векторе. Решение, соответствующее принятым нормам В документации на интерфейс Iterator<E>, входящий в состав прикладного интерфейса Java API [API 2013], говорится следующее: “Интерфейс Iterator заменяет интерфейс Enumeration в каркасе коллекций Java. Итераторы отличаются от перечислений следующим: они позволяют вызывающему коду удалять элементы из базовой коллекции во вре- мя ее обхода с вполне определенной семантикой; имена методов усовершенствованы”. В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, устраняется описанное выше затруднение в коде, не соответствую- щем принятым нормам, а также демонстрируются преимущества применения интерфейса Iterator над интерфейсом Enumeration. class Bankoperations { private static void removeAccounts(Vector v, String name) { Iterator i = v.iterator(); while (i.hasNext()) { String s = (String) i.next(); if (s.equals(name)) { i.remove () ; // экземпляры имени Harry удаляются правильно } } // отобразить текущих владельцев счетов System.out.printin("The names are:"); i = v.iterator(); while (i.hasNext()) { System.out .printin (i .next () ); // вывести только имена Dick и Tom } }
166 Глава 3 Надежность public static void main (String args[]) { List list = new ArrayList(Arrays.asList( new String[] {"Dick", "Harry", "Harry", "Tom"})); Vector v = new Vector (list); remove(v, "Harry"); } } Применимость Применение интерфейса Enumeration для выполнения операций удаления из итерируе- мой коллекции типа Collection может привести к неожиданному поведению программы. Библиография [API 2013] Interface Enumerations Interface Iterators [Daconta 2003] Item 21, "Use Iteration over Enumeration" 48. He пользуйтесь прямыми буферами для хранения нечасто используемых объектов с коротким сроком действия Классы из пакета java.nio новой системы ввода-вывода (NIO) позволяют создавать и применять прямые буферы, которые существенно повышают пропускную способность опе- раций ввода-вывода. Но их создание и удаление обходится дороже, чем создание и удале- ние непрямых буферов в выделяемой динамической области памяти, поскольку управление прямыми буферами осуществляется с помощью собственного кода, характерного для опе- рационной системы. Вследствие этих дополнительных затрат на управление прямые буферы не годятся для хранения однократно или нечасто используемых объектов. Кроме того, пря- мые буферы оказываются за пределами области действия системы “сборки мусора” в Java. Следовательно, опрометчивое применение прямых буферов может привести к утечкам па- мяти. И наконец, частое выделение крупных прямых буферов может вызвать ошибки типа OutOfMemoryError. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам надежного программи- рования на Java, применяется как локальный объект типа rarelyUsedBuf fer с коротким сроком действия, так и часто используемый объект типа heavilyUsedBuf fer с длительным сроком действия. Для обоих объектов выделяется нединамическая память, но при этом “му- сор” не собирается. ByteBuffer rarelyUsedBuffer = ByteBuffer.allocateDirect(8192); // использовать объект типа rarelyUsedBuffer однократно ByteBuffer heavilyUsedBuffer = ByteBuffer.allocateDirect(8192); // использовать объект типа heavilyUsedBuffer многократно
49. Удаляйте объекты с коротким сроком действия из контейнерных объектов... 167 Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, применяется непрямой буфер для хранения нечасто используемого объекта с коротким сроком действия. А в качестве интенсивно применяемого буфера про- должает надлежащим образом использоваться прямой буфер без динамического выделения памяти и “сборки мусора”. ByteBuffer rarelyUsedBuffer = ByteBuffer.allocate(8192); // использовать объект типа rarelyUsedBuffer однократно ByteBuffer heavilyUsedBuffer = ByteBuffer .allocateDirect (8192); // использовать объект типа heavilyUsedBuffer многократно Применимость Прямые буфера оказываются за пределами области действия системы “сборки мусора” в Java и могут вызывать утечки памяти, если применяются опрометчиво. Как правило, прямые буфера должны выделяться только в том случае, если их применение дает существенный вы- игрыш в производительности. Библиография [API 2013] Class ByteBuffer 49. Удаляйте объекты с коротким сроком действия из контейнерных объектов с длительным сроком действия По завершении задачи следует всегда удалять объекты с коротким сроком действия из контейнерных объектов с длительным сроком действия. Например, объекты, присоединяе- мые к объекту типа java. nio. channels. SelectionKey, должны быть удалены, если они больше не нужны. Благодаря этому уменьшается вероятность утечек памяти. Аналогично, для того чтобы обозначить отсутствие записи, следует явно устанавливать пустое значение отде- льного элемента массива типа ArrayList. Данная рекомендация имеет непосредственное отношение к объектам, обращение к которым осуществляется из контейнеров. Пример, демонстрирующий, что обнуление объ- ектов не помогает “сборке мусора”, приведен в рекомендации 75 “Не пытайтесь оказывать помощь системе “сборки мусора”, устанавливая пустое значение в локальных переменных ссылочного типа”. Пример кода, не соответствующего принятым нормам (удаление объектов с коротким сроком действия) В данном примере кода, не соответствующего принятым нормам надежного программи- рования на Java, списочный массив типа ArrayList с длительным сроком действия содержит ссылки на элементы как с коротким, так и с длительным сроком действия. Элементы, которые стали больше ненужными, помечаются как “пассивные” установкой соответствующего при- знака в объекте.
168 Глава 3 Надежность class DataElement { private boolean dead = false; / / другие поля public boolean isDeadO { return dead; } public void killMeO { dead = true; } } I I... где-нибудь в другом месте кода List<DataElement> longLivedList = new ArrayList<DataElement>(); // обработка, в ходе которой элемент делается ненужным // удалить элемент, который больше не нужен longLivedList .get (somelndex) .killMeO ; Система “сборки мусора” не в состоянии удалить пассивный объект типа DataElement из оперативной памяти до тех пор, пока он не станет объектом без ссылки. Следует, однако, иметь в виду, что все методы, оперирующие объектами класса DataElement, должны также проверять, является ли рассматриваемый экземпляр пассивным. Решение, соответствующее принятым нормам (установка пустой ссылки) В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, вместо установки признака пассивности элементам списочного массива типа ArrayList, ставшим пассивными, присваивается пустое значение (null). Следует, однако, иметь в виду, что во всем коде, оперирующем списочным массивом типа longLivedList, теперь должны проверяться пустые элементы списка. class DataElement { // признак пассивности удален // другие поля } // где-нибудь в другом месте кода List<DataElement> longLivedList = new ArrayList<DataElement>(); // обработка, в ходе которой элемент делается ненужным // установить пустую ссылку на ненужный объект типа DataElement longLivedList.set(somelndex, null); Решение, соответствующее принятым нормам (применение шаблона "Пустой объект") В представленном здесь решении, соответствующем принятым нормам надежного про- граммирования на Java, с помощью одиночного часового объекта устраняются осложнения, связанные с намеренно пустыми ссылками. Такая методика называется шаблоном “Пустой объект”, а иначе — шаблоном “Часовой”. class DataElement { public static final DataElement NULL = createSentinel(); // признак пассивности удален // другие поля
49. Удаляйте объекты с коротким сроком действия из контейнерных объектов... 169 private static final DataElement createSentinel() { // выделить память под часовой объект, установив во всех его полях // тщательно выбранные значения, указывающие на отсутствие действий } } // где-нибудь в другом месте кода List<DataElement> longLivedList = new ArrayList<DataElement>(); // обработка, в ходе которой элемент делается ненужным // установить пустой объект в качестве ссылки на ненужный // объект типа DataElement longLivedList.set(somelndex, DataElement.NULL); Предпочтение следует по возможности отдавать данному шаблону проектирования над явно указываемыми пустыми ссылками, как поясняется в рекомендации 41 “Возвращайте из методов пустой массив или коллекцию вместо пустого значения” При использовании данного шаблона проектирования пустой объект должен быть одиночным и конечным, но не откры- тым или закрытым в зависимости от общей конструкции класса DataElement. Состояние пустого объекта должно быть неизменяемым после его создания, что обеспечивается с по- мощью конечных полей или явного кода в методах из класса DataElement. Дополнительные сведения по данному вопросу приведены в главе 8 “Поведенческие шаблоны, шаблон “Пустой объект” из книги Patterns in Java, Volume 1, Second Edition [Grand 2002], а также в рекомен- дации “ERR08-J. He перехватывайте исключение типа NullPointerException и любые его предшественники” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012] Применимость Если оставить объекты с коротким сроком действия в контейнерных объектах с длитель- ным сроком действия, то они могут потреблять ресурсы оперативной памяти, которые нельзя освободить в результате “сборки мусора”. А это, в свою очередь, приведет к исчерпанию опе- ративной памяти и возможным атакам типа отказа в обслуживании. Библиография [Grand 2002] Chapter 8, "Behavioral Patterns, the Null Object" [Long 2012] ERR08-J. Do not catch NullPointerException or any of its ancestors
Глава 4 Понятность программ Понятность программы означает простоту ее понимания, т.е. способность определить назначение и принцип действия программы, читая ее исходный код и сопутствующую доку- ментацию [Grubb 2003]. Понятный код легче сопровождать, поскольку в этом случае меньше вероятность внесения неисправностей в код, который ясен и постижим. Понятность помогает и в ручном анализе исходного кода, потому что она упрощает обнаружение неисправностей и уязвимостей в коде при его ревизии. Некоторые рекомендации, приведенные в этой главе, носят стилистический характер. Они помогают программирующим на Java писать более удобочитаемый и ясный код. Пренебре- жение этими рекомендациями может стать причиной проектных недочетов и непонятности кода. 50. Будьте внимательны, применяя визуально дезориентирующие идентификаторы и литералы Пользуйтесь визуально различаемыми идентификаторами, которые вряд ли будут истол- кованы неправильно во время разработки и просмотра исходного кода. Некоторые символы визуально кажутся одинаковыми и могут быть неверно восприняты в зависимости от приме- няемых шрифтов, как показывают примеры, приведенные в табл. 4.1. Таблица 4.1. Дезориентирующие символы Предполагаемый символ Может быть неверно принят за этот символ, и наоборот 0 (нуль) О (прописная буква о) D (прописная буква d) 1 (единица) I (прописная буква i)
172 Глава 4 Понятность программ Окончание табл. 4.1 Предполагаемый символ Может быть неверно принят за этот символ, и наоборот 2 (два) 5 (пять) 8 (восемь) п (строчная буква N) гп (строчная буква R, строчная буква N) 1 (строчная буква L) Z (прописная буква z) S (прописная буква s) В (прописная буква Ь) h (строчная буква Н) m (строчная буква М) В спецификации языка программирования Java (JLS) предписывается, что исходный код программы может быть написан с помощью кодировки символов в уникоде (Unicode) [Unicode 2013]. Некоторые символы в уникоде имеют одинаковое визуальное представление при отображении многими распространенными шрифтами. Например, буквы греческого и коптского алфавитов (т.е. символы уникода в пределах от 0370 до 03FF) нередко трудно от- личаемы от применяемых в математике буквенно-цифровых знаков из поднабора символов греческого алфавита (т.е. символов уникода в пределах от 1D400 до 1D7FF). Избегайте определения идентификаторов, включающих в себя символы уникода с пере- загружаемыми символическими знаками. Один из простых подходов заключается в исполь- зовании только символов в коде ASCII или Latin-1 для обозначения идентификаторов. Следу- ет, однако, иметь в виду, что набор символов в коде ASCII является поднабором символов в уникоде. Старайтесь не пользоваться идентификаторами, отличающимися только одним или несколькими визуально похожими символами. Кроме того, делайте начальные части длинных идентификаторов более различимыми, чтобы их было легче распознавать. По этому поводу в §3.10.1 “Целочисленные литералы” спецификации JLS [JLS 2013] говорится следующее: “Целочисленный литерал должен быть отнесен к типу long, если он оканчивается на прописную букву L или строчную букву 1 в коде ASCII. В противном случае его сле- дует отнести к типу int. Прописную букву L следует предпочесть строчной букве 1 в качестве суффикса, поскольку строчную букву 1 (эль) зачастую трудно отличить от цифры 1 (один)” Следовательно, для того чтобы ясно показать свои намерения, употребляйте прописную букву L вместо строчной буквы 1, обозначая тип long целочисленного литерала. А целочис- ленные литералы с начальными нулями фактически обозначают восьмеричные, а не десяти- чные значения. И по этому поводу в §3.10.1 “Целочисленные литералы” спецификации JLS [JLS 2013] говорится следующее: “Восьмеричное число состоит из цифры 0 (нуль) в коде ASCII и одной или больше цифр от 0 до 7 в коде ASCII, перемежающихся знаками подчеркивания, и может обоз- начать положительное, нулевое или отрицательное целочисленное значение” Такое неверное истолкование может привести к ошибкам программирования, которые, скорее всего, возникают при объявлении нескольких констант и попытке улучшить формати- рование дополнением нулями.
50. Будьте внимательны, применяя визуально дезориентирующие идентификаторы... 173 Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, две переменные, stem и stern, объявляются в одной и той же области дейс- твия. Их можно легко и неумышленно спутать по одинаковым начальным буквам в именах. int stem; // местоположение у носа судна /* ... */ int stern; // местоположение у кормы судна Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, путаница в именах переменных устраняется благодаря присваиванию им визуально различимых идентификаторов. int bow; // местоположение у носа судна /* ... */ int stern; // местоположение у кормы судна Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, при выводе результата складываются значения типа int и long, хотя эта операция может быть воспринята как сложение двух целых чисел (11111). public class Visual { public static void main(String[] args) { System.out.printIn(11111 + 11111); } } Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, вместо строчной буквы 1 употребляется прописная буква L, чтобы снять неоднозначность визуального восприятия второго целочисленного слагаемого. Приве- денный ниже код ведет себя точно так же, как и код из представленного выше примера, не со- ответствующего принятым нормам, но в данном случае намерение программиста совершенно очевидно. public class Visual { public static void main(String[] args) { System.out.printin(11111 + 1111L); } } Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного програм- мирования на Java, десятичные значения сохраняются в массиве вместе с восьмеричными
174 Глава 4 Понятность программ значениями. Кажется, что третий элемент массива предназначен для хранения не восьмерич- ного, а десятичного значения 42. Но на самом деле этому элементу массива присваивается восьмеричное значение 0042, соответствующее десятичному значению 34. int[] array = new int[3]; void exampleFunction() { array[0] = 2719; array[l] = 4435; array[2] = 0042; // ... } Решение, соответствующее принятым нормам Если целочисленные литералы предназначаются для представления десятичных значений, то следует избегать их дополнения начальными нулями. Вместо этого их лучше дополнить пробелами, чтобы сохранить выравнивание цифр. int[] array = new int[3]; void exampleFunction() { array[0] = 2719; array[1] = 4435; array[2] = 42; // ... } Применимость Если не употреблять визуально различимые идентификаторы, то в конечном итоге это может привести к неверному использованию идентификатора, а следовательно, и к неожи- данному поведению программы. Эвристическое обнаружение идентификаторов с визуально сходными именами осуществляется просто. Если спутать строчную букву 1 (эль) с цифрой 1 (один) при обозначении целочисленного значения типа long, то результаты вычислений окажутся неверными. Автоматическое обнаружение идентификаторов осуществляется напря- мую. Совместное употребление десятичных и восьмеричных значений может стать причиной неверной операции инициализации или присваивания. Обнаружить целочисленные литералы с начальными нулями просто, но определить намерение программиста использовать восьме- ричный или же десятичный литерал практически невозможно. Соответственно невозможным оказывается и надежное автоматическое обнаружение литералов. И в этом случае могут ока- заться полезными эвристические проверки. Библиография [Bloch 2005] [JLS 2013] [Seacord 2009] [Unicode 2013] Puzzle 4, "It's Elementary" §3.10.1, "Integer Literals" DCL02-C. Use visually distinct identifiers
51. Избегайте неоднозначной перегрузки методов... 175 51. Избегайте неоднозначной перегрузки методов с переменным количеством аргументов Переменное количество аргументов было внедрено в версии JDK vl. 5.0 для поддержки методов, способных принимать нефиксированное число аргументов. По этому поводу в доку- ментации на версию языка Java SE 6 [Oracle 2011b] говорится следующее: “Разработчик прикладного интерфейса API должен экономно пользоваться методами с переменным количеством аргументов и лишь в тех случаях, когда это действительно приносит заметные выгоды. Вообще говоря, методы с переменным количеством ар- гументов не следует перегружать, иначе другим программистам будет трудно понять, какой из перегружаемых методов вызывается”. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программиро- вания на Java, перезагрузка методов с переменным количеством аргументов делает неясным, какое же из определений метода displayBooleans () вызывается. class Varargs { private static void displayBooleans(boolean... bool) { System.out.print("Number of arguments: ” + bool.length + ", Contents: ’’); for (boolean b : bool) { System.out.print("[” + b + ’’]"); } } private static void displayBooleans(boolean booll, boolean bool2) { System.out .printin (’’Overloaded method invoked"); } public static void main(String[] args) { displayBooleans(true, false); } } При выполнении данного кода выводится приведенное ниже сообщение, поскольку опре- деление метода с фиксированным количеством аргументов оказывается более конкретным, а следовательно, оно лучше подходит для предоставляемых аргументов. Впрочем, такой слож- ности лучше избегать. Overloaded method invoked (Вызывается перегружаемый метод)
176 Глава 4 я Понятность программ Решение, соответствующее принятым нормам Во избежание перегрузки методов с переменным количеством аргументов следует упот- реблять отчетливые имена, чтобы вызывать нужные методы, как показано в представленном ниже решении, соответствующем принятым нормам понятного программирования на Java. class Varargs { private static void displayManyBooleans(boolean... bool) { System.out.print("Number of arguments: " + bool.length + ", Contents: "); for (boolean b : bool) { System.out.print("[" + b + "]"); private static void displayTwoBooleans(boolean booll, boolean bool2) { System.out.printIn("Overloaded method invoked"); System.out.printin("Contents: [" + booll + "], [" + bool2 + "re- public static void main(String[] args) ( displayManyBooleans(true, false); Применимость В результате опрометчивого применения перегружаемых методов с переменным количест- вом аргументов может появиться неоднозначность и ухудшиться удобочитаемость исходного кода. В то же время может возникнуть потребность нарушить данное правило из сообра- жений производительности. Это может потребоваться, например, для того, чтобы избежать лишних затрат на создание экземпляра массива и его инициализации при каждом вызове ме- тода [Bloch 2008]. public void foo() { } public void foo(int al) { } public void foo(int al, int a2, int... rest) { } Перегружая методы с переменным количеством аргументов, очень важно избегать любой неоднозначности в отношении вызова конкретных методов. Так, в приведенном выше фраг- менте кода исключается возможность неверного выбора метода благодаря однозначным сиг- натурам перегружаемых методов. Автоматическое обнаружение этих методов осуществляется напрямую. Библиография [Bloch 2008] [Steinberg 2008] [Oracle 2011b] Item 42, "Use Varargs Judiciously" Using the Varargs Language Feature Varargs
52. Избегайте внутренних индикаторов ошибок 177 52. Избегайте внутренних индикаторов ошибок Внутренний индикатор ошибки представляет собой значение, возвращаемое методом и обозначающее допустимое возвращаемое значение или же недопустимое значение, указыва- ющее на ошибку. Ниже приведены некоторые типичные примеры внутренних индикаторов ошибок. Достоверный объект или пустая ссылка. Целочисленное положительное значение или значение -1, обозначающее возникшую ошибку. Массив достоверных объектов или пустая ссылка, обозначающая отсутствие достовер- ных объектов. (Этот вопрос дополнительно рассматривается в рекомендации 41 “Воз- вращайте из методов пустой массив или коллекцию вместо пустого значения”) Внутренние индикаторы ошибок требуют от вызывающего кода проверки ошибок, но та- кая проверка нередко упускается из виду. Пренебрежение проверкой подобных условий воз- никновения ошибок не только нарушает рекомендацию “EXP00-J. Не пренебрегайте значения- ми, возвращаемыми методами” из стандарта The CERT* Oracle9 Secure Coding Standard for Java"* [Long 2012], но также имеет неудачный эффект распространения недействительных значений, которые могут быть интерпретированы как действительные при последующих вычислениях. В связи с изложенным выше следует избегать употребления внутренних индикаторов ошибок. Они намного менее распространены в базовой библиотеке Java, чем в библиотеках других языков программирования. Тем не менее подобные индикаторы употребляются в се- мействах методов read (byte [ ] b, intoff, int len) и read (char [] cbuf, intoff, int len) из пакета j ava. io. Для того чтобы обозначить исключительную ситуацию в Java, лучше сгенерировать ис- ключение, чем возвратить код ошибки. Исключения распространяются за пределы областей действия и не могут быть проигнорированы так же легко, как и коды ошибок. При использо- вании исключений код обнаружения ошибок и обработки исключений находится в стороне от основного потока управления программой. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программиро- вания на Java, предпринимается попытка чтения символов из массива и ввода дополнительно- го символа в буфер сразу же после чтения символов. static final int MAX = 21; static final int MAX_READ = MAX - 1; static final char TERMINATOR = ’ W; int read; char [] chBuff = new char[MAX]; BufferedReader buffRdr; // установить буферизированный поток чтения типа buffRdr read = buffRdr.read(chBuff, 0, MAX_READ); chBuff[read] = TERMINATOR;
178 Глава 4 Понятность программ Но если во входном буфере первоначально окажется признак конца файла, то метод read () возвратит значение -1. А попытка разместить в буфере символ окончания приведет к генерированию исключения типа ArraylndexOutOfBoundsException. Решение, соответствующее принятым нормам (заключение в оболочку) В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, определяется метод readSafe (), заключающий в оболочку исходный метод read () и генерирующий исключение при обнаружении конца файла. public static int readSafe(BufferedReader buffer, char[] cbuf, int off, int len) throws lOException { int read = buffer.read(cbuf, off, len); if (read == -1) { throw new EOFException(); } else { return read; // ... BufferedReader buffRdr; // установить буферизированный поток чтения типа buffRdr try { read = readSafe(buffRdr, chBuff, 0, MAX_READ); chBuff[read] = TERMINATOR; } catch (EOFException eof) { chBuff[0] = TERMINATOR; Применимость Применение внутренних индикаторов ошибок может привести к пренебрежению провер- ками кодов состояния или возврату неверных значений, а следовательно, и к неожиданному поведению программы. Учитывая относительно редкое употребление внутренних индикато- ров в Java, можно составить список всех стандартных библиотечных методов, в которых они все же применяются, а также организовать их автоматическое обнаружение. Но в целом обна- ружить безопасное применение внутренних индикаторов ошибок не так-то просто. Возврат объекта, который может оказаться пустым при неудачном исходе или действи- тельным при удачном исходе, служит типичным примером применения внутреннего инди- катора ошибок. И хотя существуют более совершенные способы разработки методов, тем не менее возврат объекта, который может оказаться пустым, вполне допустим при определенных обстоятельствах. Характерный тому пример приведен в рекомендации 26 “Всегда предостав- ляйте отклик на результирующее значение метода”. Библиография [API 2013] Class Reader [JLS 2013] Chapter 11, "Exceptions" [Long 2012] EXPOO-J. Do not ignore values returned by methods
53. Не выполняйте операции присваивания в условных выражениях 179 53. Не выполняйте операции присваивания в условных выражениях Применение оператора присваивания в условных выражениях нередко обозначает ошибки программирования, а следовательно, и неожиданное поведение кода. Оператор присваивания не должен применяться в следующих контекстах. Условный оператор if (управляющее выражение). Цикл while (управляющее выражение). Цикл do ... while (управляющее выражение). Цикл for (второй операнд). Оператор switch (управляющее выражение). Условный оператор ?: (первый операнд). Логический оператор && (любой операнд). Логический оператор | | (любой операнд). Условный оператор ?: (второй или третий операнды), где тернарное выражение ис- пользуется в любом из этих контекстов. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, в качестве управляющего выражения в условном операторе if служит выра- жение присваивания. public void f(boolean a, boolean b) { if (a = b) { /* ... */ } } Несмотря на то что программист первоначально собирался присвоить переменной а зна- чение переменной Ь и проверить полученный результат, такое ошибочное употребление опе- ратора присваивания вместо оператора равенства = нередко встречается в практике про- граммирования на Java. Решение, соответствующее принятым нормам Условный блок, применяемый в представленном здесь решении, соответствующем при- нятым нормам понятного программирования на Java, выполняется только в том случае, если значения переменных а и Ь равны. И в данном случае присваивания переменной а значения переменной b не происходит. public void f(boolean a, boolean b) { if (a == b) { /* ... */ } }
180 Глава 4 Понятность программ Решение, соответствующее принятым нормам Если присваивание делается намеренно, то и намерение программиста становится вполне очевидным, как демонстрируется в приведенном ниже решении, соответствующем принятым нормам понятного программирования на Java. public void f(boolean a, boolean b) { if ((a = b) == true) { /* ... */ } } Решение, соответствующее принятым нормам Намного яснее выразить логику в виде явного присваивания и последующего условного оператора if, как показано ниже. public void f(boolean a, boolean b) { a = b; if (a) { /* ... */ } } Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, выражение присваивания применяется в качестве одного из операндов логи- ческого оператора &&. public void f(boolean a, boolean b, boolean flag) { while ( (a = b) && flag ) { /* ... */ } } Логический оператор && не является оператором сравнения, и поэтому присваивание ока- зывается недопустимым в качестве его операнда. Опять же применение оператора присваи- вания = вместо оператора равенства = относится к часто встречающимся ошибкам програм- мирования. Решение, соответствующее принятым нормам Если присваивание переменной а значения переменной Ь происходит непреднамеренно, то приведенный ниже условный блок кода выполняется только в том случае, если переменные а и b равны, а переменная flag принимает логическое значение true. Применимость Применение оператора присваивания в управляющих условных выражениях нередко озна- чает ошибку программирования и может привести к неожиданному поведению кода. В качес- тве исключения из данной рекомендации оператор присваивания разрешается использовать
54. Пользуйтесь фигурными скобками в теле условного оператора... 181 в условных выражениях, когда присваивание не относится к управляющему выражению, т.е. не является подвыражением, как показано в приведенном ниже решении, соответствующем принятым нормам понятного программирования на Java. public void assignNocontrol(BufferedReader reader) throws lOException { String line; while ((line = reader.readLine()) != null) { // ... обработка строки кода } } Библиография [Hatton 1995] §2.7.2, "Errors of Omission and Addition" 54. Пользуйтесь фигурными скобками в теле условного оператора if, а также циклов for или while Открывающие и закрывающие фигурные скобки в условном операторе if, а также в цик- лах for и while следует применять даже в том случае, если их тело содержит только один оператор. Фигурные скобки улучшают единообразие и удобочитаемость исходного кода. Еще важнее, что очень легко забыть фигурные скобки при вводе дополнительных операторов в тело оператора, содержащее лишь один оператор, поскольку принятые в программах отступы дают ясное, но все же вводящее в заблуждение представление о структуре программы. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программиро- вания на Java, аутентификация пользователя осуществляется с помощью условного оператора if без фигурных скобок. Отступы в исходном коде искажают функциональные возможности программы, что может привести к уязвимости в ее защите. int login; if (invalid_login()) login = 0; else login = 1; int login; if (invalid_login()) login = 0; else // отладить добавленную ниже строку кода System.out.printin("Login is valid\n"); // следующая строка кода выполняется всегда login = 1;
182 Глава 4 Понятность программ Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного программирования на Java, открывающие и закрывающие фигурные скобки применяют- ся, несмотря на то, что в теле условных операторов if и else содержится единственный оператор. int login; if (invalid_login()) { login = 0; } else { login = 1; Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программиро- вания на Java, один условный оператор if вкладывается в другой без фигурных скобок вокруг тела условных операторов if и else. int privileges; if (invalid_login()) if (allow_guests()) privileges = GUEST; else privileges = ADMINISTRATOR; Применение отступов в приведенном выше примере кода может навести на неверную мысль о том, что пользователям предоставляются привилегии администратора только в том случае, если их регистрационные данные действительны. Но на самом деле условный опера- тор else связан с внутренним условным оператором if, как показано ниже. Следовательно, такой недочет в исходном коде позволяет несанкционированным пользователям получить привилегии администратора. int privileges; if (invalid_login()) if (allow_guests()) privileges = GUEST; else privileges = ADMINISTRATOR; Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, фигурные скобки служат для исключения неоднозначности. Следова- тельно, привилегии администратора назначаются правильно. int privileges; if (invalid—login()) { if (allow—guests()) { privileges = GUEST;
55. Не ставьте точку с запятой сразу после условного выражения... 183 } } else { privileges = ADMINISTRATOR; } Применимость Если не заключить тело условного оператора if, а также цикла for или while в фигурные скобки, то код становится подверженным ошибкам. А это влечет за собой увеличение затрат на его сопровождение. Библиография [GNU 2013] §5.3, "Clean Use of С Constructs" 55. He ставьте точку с запятой сразу после условного выражения с оператором if, for или while Старайтесь не ставить точку с запятой сразу после условного выражения с оператором if, for или while. Ведь это, как правило, обозначает ошибку программирования и может привести к неожиданному поведению кода. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, точка с запятой ставится сразу же после условного оператора if. В итоге операторы в явно выделенном теле условного оператора if всегда выполняются независимо от результата вычисления условного выражения. if (а == Ь); { /* ... */ } Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, точка с запятой устраняется и тем самым гарантируется, что опера- торы в теле условного оператора if будут выполняться только в том случае, если условное выражение истинно. if (а == Ь) { /* ... */ } Применимость Установка точки с запятой сразу после условного выражения с оператором if, for или while может привести к неожиданному поведению кода. Библиография [Hatton 1995] §2.7.2, "Errors of Omission and Addition"
184 Глава 4 Понятность программ 56. Завершайте каждый набор операторов, связанных с меткой case, оператором break Блок оператора switch состоит из ряда ветвей с метками case и необязательной, но реко- мендуемой ветви с меткой label. Операторы, следующие после каждой метки case, должны завершаться оператором break, отвечающим за передачу управления в конец блока оператора switch. Если же оператор break опускается, то выполняются операторы в последующей вет- ви с меткой case. Указывать оператор break необязательно, и если опустить его, то никаких предупреждений от компилятора не последует. Когда же такое поведение оказывается непред- намеренным, оно может вызвать неожиданное поведение потока управления программой. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, в ветви case, где переменная card принимает значение 11, отсутствует опе- ратор break. В итоге выполнение продолжается с операторов в ветви case, где переменная card принимает значение 12. int card = 11; switch (card) { /* ... */ case 11: System.out.printin("Jack"); case 12: System.out.printin("Queen"); break; case 13: System.out.printin("King"); break; default: System.out.printin("Invalid Card"); break; Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, каждая ветвь с меткой case завершается оператором break. int card = 11; switch (card) { /* ... */ case 11: System.out.printin("Jack"); break; case 12: System.out.printin("Queen") ; break; case 13: System.out.printin("King");
56. Завершайте каждый набор операторов... 185 break; default: System.out.printin("Invalid Card"); break; } Применимость Если не указать операторы break в ветвях с метками case оператора switch, это мо- жет вызвать неожиданное поведение потока управления программой. В то же время оператор break можно опустить в конце последней ветви оператора switch, которую принято обозна- чать меткой default. Оператор break служит для передачи управления в конец блока опе- ратора switch. В его отсутствие возникает так называемый “провал” в результате которого управление также передается в конец блока оператора switch. Следовательно, управление передается операторам, следующим после блока оператора switch, независимо от наличия или отсутствия оператора break. Тем не менее последняя ветвь оператора switch должна непременно завершаться оператором break в соответствии с принятым стилем понятного программирования на Java [Allen 2000]. В исключительных случаях, когда один и тот же код требуется выполнять в нескольких ветвях с меткой case, операторы break можно опустить во всех остальных ветвях, кроме последней. Аналогично, если обработка данных в одной ветви служит подходящим префик- сом для обработки данных в одной или нескольких других ветвях с меткой case, то оператор break можно опустить в префиксной ветви с меткой case. Это обстоятельство должно быть надлежащим образом разъяснено в комментарии, как показано в приведенном ниже приме- ре. А если ветвь с меткой case завершается оператором return или throw или же вызовом метода, не возвращающим значение, например System.exit О, то оператор break может быть опущен. int card = 11; int value; // ветви с метками case 11, 12, 13 "проваливаются" через // одну и ту же ветвь switch (card) { // для обработки данных в данной ветви требуется префикс // действий в трех последующих ветвях case 10: do_something(card); // Намеренный "провал". // Три последующие ветви с меткой case интерпретируются одинаково case 11: // здесь оператор break не требуется case 12: // здесь оператор break не требуется case 13: value = 10; break; // а здесь оператор break требуется default: // обработать ошибочное условие } Библиография [JLS 2013] §14.11, "The switch Statement"
186 Глава 4 Понятность программ 57. Избегайте неумышленного зацикливания счетчиков циклов Если не запрограммировано специально, то цикл while или for может выполняться бесконечно или, по крайней мере, до тех пор, пока его счетчик не совершит циклический сдвиг и не достигнет конечного значения. (См. также рекомендацию “NUM00-J. Выявляйте или предотвращайте переполнение целочисленных операций” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012].) Подобное затруднение может возникнуть в ре- зультате инкрементирования или декрементирования счетчика цикла на более чем единицу и последующей его проверки на равенство указанному значению, чтобы завершить цикл. В данном случае вполне возможно, что счетчик цикла будет проскакивать заданное значение, а следовательно, цикл будет выполняться бесконечно или, по крайней мере, до тех пор, пока его счетчик не совершит циклический сдвиг и не достигнет конечного значения. Данное за- труднение может также возникнуть в результате незамысловатой проверки пределов. Напри- мер, цикл должен выполняться до тех пор, пока счетчик цикла меньше или равен значению Integer .MAX_VALUE либо больше или равен значению Integer .MIN_VALUE. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, цикл должен бы повторяться пять раз, но на самом деле он выполняется бесконечно. Переменная цикла i принимает последовательные значения 1, 3, 5, 7, 9,11 и т.д., а следовательно, сравнение со значением 10 так и не дает истинного результата. В итоге зна- чение переменной i достигает максимальной величины представления целых положительных целых чисел (Integer .MAX_VALUE), а затем происходит циклический сдвиг до предпослед- ней наименьшей величины отрицательных целых чисел (Integer .MIN_VALUE + 1). После этого счетчик цикла достигает значения -1, затем 1, и далее цикл продолжается, как описано ранее. for (i = 1; i != 10; i += 2) { // ... } Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программиро- вания на Java, цикл завершается, но повторяется много больше, чем предполагалось. for (i = 1; i != 10; i += 5) { // ... } Переменная цикла i принимает последовательные значения 1, б и 11, пропуская значение 10. Затем происходит циклический сдвиг значения переменной цикла i от близкого к мак- симальной положительной величине до близкой к минимальной отрицательной величине и в обратном направлении до нуля. Далее переменная цикла i принимает последовательные зна- чения 2, 7 и 12, пропуская значение 10. После циклического сдвига от максимальной поло- жительной величины до минимальной отрицательной величины три раза подряд переменная цикла i достигает наконец последовательных значений 0, 5 и 10, и на этом цикл завершается.
57. Избегайте неумышленного зацикливания счетчиков циклов 187 Решение, соответствующее принятым нормам Одно из решений состоит в том, чтобы обеспечить достижение условия завершения цик- ла, прежде чем произойдет неумышленное зацикливание счетчика цикла. for (i = 1; i == 11; i += 2) { // ... } Данное решение может оказаться уязвимым, когда требуется изменить одно или больше условие, оказывающее влияние на повторение цикла. Лучшее решение состоит в том, что- бы воспользоваться оператором числового сравнения (т.е. <, <=, > или >=), чтобы завершить цикл, как показано ниже. for (i = 1; i <= 10; i += 2) { // ... } Это последнее решение может оказаться более надежным при изменениях в условиях пов- торения цикла. Тем не менее такой подход вряд ли заменит тщательное рассмотрение предпо- лагаемого и фактического количества повторений цикла. Пример кода, не соответствующего принятым нормам Выражение, в котором проверяется, является ли счетчик цикла меньше или равным значе- нию Integer .MAX_VALUE или же больше или равным значению Integer .MIN_VALUE, вряд ли позволит завершить цикл, поскольку вычисление этого выражения дает истинный резуль- тат. Например, приведенный ниже цикл никогда не завершится, поскольку переменная цикла i так и не превысит значение Integer .MAX_VALUE. for (i = 1; i <= Integer.MAX-VALUE; i++) { // ... } Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, цикл завершается, когда переменная цикла i равна значению Integer.MAX_VALUE. for (i = 1; i < Integer.MAX_VALUE; i++) { // ... } Если цикл предназначается для повторения после каждого значения переменной цикла i больше нуля, включая значение Integer .MAX VALUE, он может быть реализован следующим образом: i = 0; do { // ... } while (i != Integer.MAX_VALUE);
188 Глава 4 Понятность программ Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, переменная цикла i сначала инициализируется нулевым значением, а затем инкрементируется на два на каждом шаге цикла, по существу, перечисляя все четные поло- жительные значения. Предполагается, что цикл должен завершиться, когда его переменная i станет больше чет- ного значения Integer .MAX_VALUE - 1. Но в данном случае цикл не завершается, потому что происходит циклический сдвиг счетчика цикла, прежде чем он станет больше значения Integer.MAX_VALUE - 1. for (i = 0; i <= Integer.MAX_VALUE - 1; i += 2) { // ... } Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, цикл завершается, когда счетчик i становится больше значения Integer .MAX_VALUE минус значение шага цикла в качестве условия завершения цикла. for (i = 0; i <= Integer.MAX_VALUE - 2; i += 2) { // ... } Применимость Неверное завершение циклов может привести к их бесконечному выполнению, низкой производительности, неверным результатам и прочим недостаткам. Если на любые условия завершения цикла оказывает воздействие совершающий атаку злоумышленник, подобные ошибки могут быть использованы для инициирования отказа в обслуживании и прочих ви- дов атак. Библиография [JLS 2013] §15.20.1, "Numerical Comparison Operators <, <=, >, and >=* [Long 2012] NUMOO-J. Detect or prevent integer overflow 58. Пользуйтесь круглыми скобками для обозначения операций предшествования Программисты нередко совершают ошибки, связанные с предшествованием операторов, поскольку степень предшествования операторов &, |, А, « и » не вполне очевидна. В связи с этим следует избегать ошибок, связанных с предшествованием операций, используя надле- жащим образом круглые скобки, что также способствует повышению удобочитаемости кода. Предшествование операций по порядку следования подвыражений приведено в учебном по- собии Java™ Tutorials [Tutorials 2013]. Несмотря на то что в рекомендации “EXP05-J. Не употребляйте одну и ту же переменную больше одного раза в выражении” из стандарта The CERT* Oracle* Secure Coding Standard for
58. Пользуйтесь круглыми скобками для обозначения операций предшествования 189 Java™ [Long 2012] не советуется особенно полагаться на круглые скобки для указания порядка вычисления, эта рекомендация все же распространяется на выражения, содержащие побоч- ные эффекты. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программиро- вания на Java, предполагается сложить значение переменной OFFSET с результатом поразряд- ной логической операции И над переменными х и MASK. public static final int MASK = 1337; public static final int OFFSET = -1337; public static int computeCode(int x) { return x & MASK + OFFSET; } В соответствии с рекомендациями относительно предшествования операторов выражение в операторе return синтаксически анализируется следующим образом: х & (MASK + OFFSET) Это выражение вычисляется так, как показано ниже, и дает в результате нулевое значение. х & (1337 - 1337) Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, с помощью круглых скобок обеспечивается вычисление выражения в требуемом порядке. public static final int MASK = 1337; public static final int OFFSET = -1337; public static int computeCode(int x) { return (x & MASK) + OFFSET; } Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программиро- вания на Java, предполагается присоединить символ ”0" или "1" к строке "value- . Но по правилам предшествования операций выражение, результат вычисления которого выводится на консоль, синтаксически анализируется как ("value=" + s) == null ? 0 : 1. public class Printvalue { public static void main(String[] args) { String s = null; // выводится символьная строка "value=l" System.out.printin("value=" + s == null ? 0 : 1);
190 Глава 4 Понятность программ Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, с помощью круглых скобок обеспечивается вычисление выражения в требуемом порядке. public class Printvalue { public static void main(String[] args) { String s = null; // выводится символьная строка "value=0", как и предполагалось System.out.printIn("value=" + (s == null ? 0 : 1)); } } Применимость Ошибки, связанные с несоблюдением правил предшествования, могут стать причиной вычисления конкретного выражения непреднамеренным образом, а следовательно, неожи- данного и ненормального режима работы программы. Круглые скобки можно опустить в тех математических выражениях, где соблюдаются правила предшествования алгебраических операций. В качестве примера рассмотрим следующее выражение: X + у * Z В математике умножение принято выполнять перед сложением. Поэтому круглые скобки в приведенном ниже выражении излишни. X + (у * Z) Обнаружить все выражения с низкой степенью предшествования операторов без круглых скобок нетрудно. А определить правильность употребления правил предшествования опера- ций в общем случае практически невозможно, хотя для этой цели могут оказаться удобными предупреждения эвристического анализа. Библиография [ESA 2005] Rule 65, Use parentheses to explicitly indicate the order of execution of numerical operators [Long 2012] EXP05-J. Do not write more than once to the same variable within an expression [Tutorials 2013] Expressions, Statements, and Blocks 59. He делайте никаких предположений о создании файлов Несмотря на то что файл обычно создается в результате единственного вызова метода, в связи с этой единственной операцией возникает немало вопросов, касающихся безопаснос- ти. В частности, что делать, если файл нельзя создать или если он уже существует, и каковы должны быть исходные атрибуты файла, в том числе и права доступа к нему? В Java предоставляется несколько поколений языковых средств для обработки файлов. Первоначальные средства ввода-вывода, включая элементарную обработку файлов, находятся
59. Не делайте никаких предположений о создании файлов 191 в пакете java. io, а более сложные средства входят в состав пакета java.nio новой системы ввода-вывода (New I/O) в версии JDK 1.4 (см. прикладные интерфейсы API New I/O [Oracle 2010b]). Еще более изощренные средства были включены в состав пакета java.nio. file сис- темы ввода-вывода New I/O 2 в версии JDK 1.7. В обоих пакетах, j ava. nio и j ava. nio. file, предоставляется целый ряд методов для поддержки более точного контроля над созданием файлов. В рекомендации “FIO01-J. Создавайте файлы с соответствующими правами доступа” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012] поясняется, каким образом следует указывать полномочия на доступ к вновь созданному файлу. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам безопасного програм- мирования на Java, предпринимается попытка открыть файл для записи в него данных. Если файл существует до его открытия, то прежнее его содержимое будет перезаписано содержи- мым, предоставляемым программой. public void createFile(String filename) throws FileNotFoundException{ Outputstream out = new FileOutputStream(filename); // обработать файл } Пример кода, не соответствующего принятым нормам (TOCTOU) В данном примере кода, не соответствующего принятым нормам безопасного программи- рования на Java, предпринимается попытка избежать изменений в существующем файле, для чего создается пустой файл с помощью метода java. io. File. createNewFile (). Так, если файл с заданным именем уже существует, то метод createNewFile () возвратит логическое значение false, не уничтожая его содержимое. public void createFile(String filename) throws FileNotFoundException{ Outputstream out = new FileOutputStream(filename, true); if (!new File(filename).createNewFile()) { // файл создать нельзя; обработать ошибку } else { out = new FileOutputStream(filename); // обработать файл } } К сожалению, рассматриваемое здесь решение страдает подверженностью гонке типа “время проверки — время использования” (TOCTOU). Поэтому совершающий атаку зло- умышленник может видоизменить файловую систему после создания пустого файла, но перед открытием файла, например, в том случае, когда открытый файл отличается от созданного. Решение, соответствующее принятым нормам (файлы) В представленном здесь решении, соответствующем принятым нормам безопасного програм- мирования на Java, метод java.nio. file. Files.newOutputStream () вызывается для авто- матического создания файла. И если такой файл уже существует, то генерируется исключение. public void createFile(String filename) throws FileNotFoundException{
192 Глава 4 Понятность программ try (Outputstream out = new BufferedOutputStream( Files.newOutputStream(Paths.get(filename), StandardOpenOption.CREATE_NEW))) { // обработать данные, направляемые в поток вывода out } catch (lOException х) { // записать данные в файл нельзя; обработать ошибку } } Применимость Возможность определить, был ли открыт существующий файл или же создан новый файл, позволяет надежнее гарантировать, что открыт или перезаписан только предполагаемый файл, тогда как на другие файлы данная операция не оказывает никакого воздействия. Библиография [API 2013] Class java. io. File Class java. nio. file. Files [Long 2012] FIOOI-J. Create files with appropriate access permissions [Oracle 2010b] New I/O APIs 60. Преобразуйте целые значения в значения с плавающей точкой для выполнения операций с плавающей точкой Опрометчивое использование целочисленных арифметических операций для расчета зна- чения, присваиваемого переменной, относящейся к типу данных с плавающей точкой, может привести к потере информации. Например, целочисленные арифметические операции всегда дают целые результаты, в которых отбрасывается информация о любом возможном дробном остатке. Более того, при преобразовании целочисленных значений в значения с плавающей точкой возможна потеря точности. (Подробнее об этом см. в рекомендации “NUM 13-J. Избе- гайте потери точности при преобразовании примитивных целых значений в значения с пла- вающей точкой” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]). Правильное программирование выражений, в которых целочисленные значения сочета- ются со значениями с плавающей точкой, требует тщательного анализа. Операции, которые могут страдать недостатками переполнения или потери дробного остатка, следует выполнять над значениями с плавающей точкой, а не над целочисленными значениями. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, операции деления и умножения выполняются над целыми значениями. А ре- зультаты этих операций затем преобразуются в тип данных с плавающей точкой. short а = 533; int Ь = 6789; long с = 4664382371590123456L;
60. Преобразуйте целые значения в значения с плавающей точкой... 193 float d = а / 7; double е = b / 30; double f = с * 2; // значение переменной d равно 76.0 (усечено) // значение переменной е равно 226.0 (усечено) // значение переменной f равно -9.1179793305293046Е18 // из-за переполнения в целочисленных операциях Результаты целочисленных операций усекаются до ближайшего целого значения и могут также привести к переполнению. В итоге значения переменных d, е и f, относящихся к типу данных с плавающей точкой, инициализируются неправильно, поскольку при преобразова- нии в тип с плавающей точкой происходит усечение и переполнение. Следует также иметь в виду, что вычисление значения переменной с противоречит рекомендации “NUM00-J. Выяв- ляйте или предотвращайте переполнение целочисленных операций” из стандарта The CERT* Oracle9 Secure Coding Standard for Java" [Long 2012]. Решение, соответствующее принятым нормам (литерал с плавающей точкой) В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, операции умножения и деления выполняются над значениями с пла- вающей точкой, и благодаря этому исключается усечение и переполнение, имевшие место в приведенном выше примере кода, не соответствующего принятым нормам понятного про- граммирования на Java. В каждой операции хотя бы один из операндов относится к типу с плавающей точкой, что принуждает к выполнению операций умножения и деления с плаваю- щей точкой, а следовательно, исключает усечение и переполнение. short а = 533; int Ь = 6789; long с = 4664382371590123456L; float d = а / 7.Of; // значение переменной d равно 76.14286 double е = b / 30.; // значение переменной е равно 226.3 double f = (double)с * 2; // значение переменной f равно 9.328764743180247Е18 Еще одно решение, соответствующее принятым нормам понятного программирования на Java, состоит в том, чтобы исключить ошибки усечения и переполнения, сохраняя целочис- ленные значения в переменных типа с плавающей точкой перед выполнением арифметичес- ких операций, как показано ниже. short а = 533; int b = 6789; long с = 4664382371590123456L; float d = а; double е = b; double f = с; d /= 7; // значение переменной d равно 76.14286 е /= 30; // // значение переменной е равно 226.3 f *= 2; // значение переменной f равно 9.328764743180247Е18 Как и в предыдущем решении, такой практический прием гарантирует, что хотя бы один из операндов каждой арифметической операции является числом с плавающей точкой. Сле- довательно, операции выполняются над значениями с плавающей точкой.
194 Глава 4 п Понятность программ В обоих рассмотренных выше решениях, соответствующих принятым нормам понят- ного программирования на Java, исходное значение нельзя представить точно с помощью типа double. Это значение типа double может быть представлено только с 48-разряд- ной мантиссой, тогда как для точного его представления требуется 56-разрядная мантис- са. Следовательно, значение переменной с округляется до ближайшей величины, которая может быть представлена с помощью типа double, а рассчитанное значение переменной f(9.328764743180247Е18) отличается от точного результата математической операции (9328564743180246912). Такая потеря точности служит одной из многих причин, по ко- торым правильное программирование выражений, где целочисленные операции или значе- ния сочетаются с операциями или значениями с плавающей точкой, требует тщательного подхода. Подробнее о потере точности при преобразовании целых значений в значения с плаваю- щей точкой см. в рекомендации “NUM 13-J. Избегайте потери точности при преобразовании примитивных целых значений в значения с плавающей точкой” из стандарта The CERT* Oracle9 Secure Coding Standard for Java™ [Long 2012]. Но даже при такой потере точности рассчитанное значение переменной f оказывается намного более точным, чем значение, получаемое в при- мере кода, не соответствующего принятым нормам понятного программирования на Java. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, предпринимается попытка рассчитать наибольшее целое значение из соотно- шения двух целых значений. В результате расчета получается значение 1.0 вместо предпола- гаемого значения 2.0. int а = 60070; int b = 57750; double value = Math.ceil(a/b); Вследствие правил продвижения числовых типов результат целочисленной операции деле- ния усекается до 1. Этот результат затем продвигается к типу double перед тем, как передать его методу Math. ceil (). Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, осуществляется приведение одного из операндов к типу double пе- ред выполнением операции деления. int а = 60070; int b = 57750; double value = Math.ceil(а/((double) b)); В результате приведения типа одного операнда другой операнд автоматически продвигает- ся к типу double. Таким образом, операция деления выполняется с точностью типа double, а ее правильный результат 2.0 присваивается переменной value. Как и в предыдущих ре- шениях, соответствующих принятым нормам понятного программирования на Java, такой практический прием гарантирует, что хотя бы один из операндов каждой арифметической операции является числом с плавающей точкой.
61. Непременно вызывайте метод super.cloneO из метода done() 195 Применимость Неправильные преобразования целых значений в значения с плавающей точкой и обратно могут привести к неожиданным результатам, особенно из-за потери точности. А иногда эти неожиданные результаты могут повлечь за собой переполнение или другие исключительные ситуации. В математических операциях целые значения допускается сочетать со значениями с плава- ющей точкой, намеренно используя свойства целочисленной арифметики перед преобразова- нием результатов в тип данных с плавающей точкой. Например, употребление целочисленной арифметики исключает необходимость вызывать метод floor (). Но любой подобный код должен быть ясно документирован, чтобы тем, кто будет сопровождать его в дальнейшем, ста- ло понятно, что такое поведение кода выбрано намеренно. Библиография [JLS 2013] §5.1.2, "Widening Primitive Conversion" [Long 2012] NUM13-J. Avoid loss of precision when converting primitive integers to floating-point NUMOO-J. Detect or prevent integer overflow 61. Непременно вызывайте метод super. clone () из метода clone () Клонирование подкласса, производного от неконечного класса, где определяется метод clone (), из которого не вызывается метод super. clone (), в конечном итоге дает объект неверного класса. По этому поводу в документации на метод clone () в прикладном интер- фейсе Java API [API 2013] говорится следующее: “По принятому соглашению возвращаемый объект должен быть получен из вы- зываемого метода super.cloneO. Если класс и все его суперклассы, кро- ме класса Object, подчиняются этому соглашению, то вполне возможно, что х.clone () .getClass () == х.getClass ().” Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам понятного программиро- вания на Java, метод super. clone () не вызывается из метода clone () в классе Base. Следо- вательно, объект devClone в конечном итоге относится к типу Base вместо типа Derived, а метод doLogic () применяется неверно. class Base implements Cloneable { public Object clone() throws CloneNotSupportedException { return new Base(); } protected void doLogic() { System.out.printin("Superclass doLogic"); } } class Derived extends Base {
196 Глава 4 Понятность программ public Object clone() throws CloneNotSupportedException { return super.clone(); } protected void doLogicO { System.out.printin("Subclass doLogic"); } public static void main(String[] args) { Derived dev = new Derived(); try { Base devClone = (Base)dev.clone(); // имеет тип Base // вместо типа Derived devClone.doLogic(); // выводит строку "Superclass doLogic" // вместо строки "Subclass doLogic" } catch (CloneNotSupportedException e) {/*...*/} } } Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, метод super.clone () правильно вызывается из метода clone () в классе Base. class Base implements Cloneable { public Object clone() throws CloneNotSupportedException { return super.clone(); } protected void doLogicO { System.out.printIn("Superclass doLogic"); } } class Derived extends Base { public Object clone() throws CloneNotSupportedException { return super.clone(); } protected void doLogicO { System.out.printin("Subclass doLogic"); } public static void main(String[] args) { Derived dev = new Derived(); try { // имеет тип Derived, как и предполагалось Base devClone = (Base)dev.clone(); devClone.doLogic(); // выводит строку "Subclass doLogic", // как и предполагалось } catch (CloneNotSupportedException e) {/*...*/} } } Применимость Если не вызвать метод super. clone (), клонированный объект может иметь неверный тип.
62. Употребляйте комментарии единообразно и в удобном для чтения виде 197 Библиография [API 2013] Class Object 62. Употребляйте комментарии единообразно и в удобном для чтения виде Употребление традиционных или блочных комментариев, начинающихся с символов /* и оканчивающихся символами */, вместе с комментариями в конце строки (от символов // и до конца строки) может привести к путанице и неверному истолкованию кода, а следова- тельно, и к ошибкам. Пример кода, не соответствующего принятым нормам В следующих примерах кода, не соответствующего принятым нормам понятного програм- мирования на Java, демонстрируется смешанное употребление разных видов комментариев, которое может привести к неверному истолкованию кода. // */ /* Это комментарий, а не синтаксическая ошибка */ f = g/**//h; /* Равнозначно выражению f = g / h; */ /*//*/ 10; /* Равнозначно выражению К); */ m = n//**/o + p; /* Равнозначно выражению m = n + р; */ a = b //*divisor: :*/c + d; /* Равнозначно выражению a = b + d; */ Решение, соответствующее принятым нормам Пользуйтесь единообразным стилем комментирования кода, как показано ниже. // простой и аккуратный комментарий int i; // это счетчик Пример кода, не соответствующего принятым нормам Имеются и другие примеры неверного употребления комментариев, которого следует из- бегать. Так, в следующем примере кода, не соответствующего принятым нормам понятного программирования на Java, начало комментария обозначается символами /*, а конец коммен- тария не ограничивается символами */. Следовательно, код с вызовом метода, имеющего ре- шающее значение для безопасности, не выполняется. Но просматривающий данную страницу исходного кода может неверно предположить, что код выполняется. /* Комментарий с намеренно опущенной меткой окончания комментария security_critical_method(); /* Другой комментарий */
198 Глава 4 Понятность программ Случайные пропуски меток окончания комментариев можно обнаружить в редакторе тек- ста, предоставляющем функции выделения синтаксиса или форматирования кода. Но опус- кать метки окончания комментариев все же не рекомендуется, поскольку это чревато ошиб- ками и зачастую считается недоразумением. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, демонстрируется рекомендуемый способ пометки кода как “пассив- ного”. В нем также используется способность компилятора удалять недоступный (пассивный) код. Код в блоке условного оператора if должен быть синтаксически правильным. Если же последующие изменения в других частях программы приведут к синтаксическим ошибкам, то невыполняемый код должен быть обновлен для устранения ошибок. И если он потребуется впоследствии, то программисту достаточно будет лишь удалить окружающий его условный оператор if и комментарий NOTREACHED (НЕДОСТУПНО). if (false) { /* Использовать метод, имеющий решающее значение для безопасности, теперь больше не нужно */ /* NOTREACHED */ security_critical_method(); /* Другой комментарий */ } Комментарий NOTREACHED может сообщить некоторым компиляторам и инструменталь- ным средствам статического анализа не обращать внимания на недоступный код. Он также служит в качестве документации. Это пример исключительной ситуации, описанной в реко- мендации 63 “Выявляйте и удаляйте излишний код и значения”. Применимость Недоразумение, возникающее в связи с выполнением одних команд и невыполнением других, может привести к серьезным ошибкам программирования и уязвимостям в защите, включая отказ в обслуживании, ненормальное завершение программы и нарушение целост- ности данных. Данное недоразумение можно отчасти устранить, используя интегрированные среды разработки (ИСР), редакторы, где текст выделяется шрифтами и цветами, а также дру- гие механизмы, чтобы отличать комментарии от кода. Но данное недоразумение по-прежнему проявляется при анализе исходного кода, напечатанного на черно-белом принтере. Вложен- ные блочные комментарии и неединообразное употребление комментариев может быть обна- ружено подходящими инструментальными средствами статического анализа. Библиография [JLS2013] §3.7, "Comments" 63. Выявляйте и удаляйте излишний код и значения Излишний код и значения могут возникнуть в форме пассивного, недействующего кода, а также неиспользуемых значений в логике программы. Код, который вообще не выполня- ется, называется пассивным, или просто “мертвым”. Как правило, присутствие пассивного кода обозначает логическую ошибку, возникшую в результате изменений в программе или
63. Выявляйте и удаляйте излишний код и значения 199 в ее среде. Пассивный код нередко исключается из программы в результате оптимизации во время компиляции. Но для повышения удобочитаемости и устранения логических ошибок пассивный код должен быть распознан, осмыслен и удален. Код, который выполняется, но не в состоянии произвести какое-нибудь действие или имеет непреднамеренный эффект, скорее всего, возникает в результате ошибок программи- рования и может привести к неожиданному поведению. Операторы или выражения, не ока- зывающие никакого воздействия, должны быть выявлены и удалены из кода. Большинство современных компиляторов способны предупреждать о коде, не оказывающем никакого воз- действия. Наличие неиспользуемых значений в коде может указывать на существенные ло- гические ошибки. Во избежание подобных ошибок неиспользуемые значения должны быть выявлены и удалены из кода. Пример кода, не соответствующего принятым нормам (пассивный код) В данном примере кода, не соответствующего принятым нормам понятного програм- мирования на Java, демонстрируется, каким образом пассивный код может быть внедрен в программу [Fortify 2013]. Выражение х != 0 во втором условном операторе if вообще не вычисляется как истинное, поскольку единственный путь, по которому переменной х может быть присвоено ненулевое значение, оканчивается оператором return. public int func(boolean condition) { int x = 0; if (condition) { x = foo (); /* обработать значение переменной x */ return x; } /* ... */ if (x != 0) { /* Этот код вообще не выполняется */ } return х; } Решение, соответствующее принятым нормам Для исправления пассивного кода придется не только определить причины, по которым он вообще выполняется, но и условия, по которым он должен выполняться, а затем разре- шить возникшее затруднение соответствующим образом. В представленном здесь решении, соответствующем принятым нормам понятного программирования на Java, предполагается, что пассивный код должен выполняться, а следовательно, тело первого условного оператора больше не должно завершаться оператором return. int func(boolean condition) { int x = 0; if (condition) { x = foo () ; /* обработать значение переменной x */ } /* ... */ if (x ! = 0) {
200 Глава 4 Понятность программ /* Теперь этот код выполняется */ } return 0; Пример кода, не соответствующего принятым нормам (пассивный код) В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, функция length () служит для ограничения количества повторных вызовов функции string loop (). Выражение в условном операторе if, находящемся в теле цик- ла for, вычисляется как истинное, когда текущее значение переменной цикла i достигает длины символьной строки str. Но этого так и не происходит, поскольку переменная цикла i всегда оказывается меньше длины символьной строки, определяемой при вызове метода str.length(). public int string_loop(String str) { for (int i=0; i < str.length(); i++) { /* ... */ if (i == str.length()) { /* Этот код вообще не выполняется */ } } return 0; Решение, соответствующее принятым нормам Правильное исправление пассивного кода зависит от конкретных намерений программис- та. Так, если его намерение состоит в особой обработке последнего символа в строке str, условный оператор следует настроить на проверку соответствия значения переменной цикла i индексу последнего символа в строке str, как показано ниже. public int string_loop(String str) { for (int i=0; i < str.length(); i++) { /* ... */ if (i == str.length()-1) { /* Теперь этот код выполняется */ } } return 0; Пример кода, не соответствующего принятым нормам (код, не оказывающий никакого воздействия) В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, сравнение строковых переменных s и t ничего не дает. Подобная ошибка возникла, вероятно, потому, что программист намеревался добиться некоторого результата сравнением символьных строк, но так и не довел код до логического завершения.
63. Выявляйте и удаляйте излишний код и значения 201 String s; String t; // ... s.equals(t); Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, результат сравнения символьных строк выводится на консоль. String s; String t; // ... if (s.equals(t)) { System.out.printIn("Strings equal"); } else { System.out.printin("Strings unequal"); } Пример кода, не соответствующего принятым нормам (неиспользуемые значения) В следующем примере кода, не соответствующего принятым нормам понятного програм- мирования на Java, переменной р2 присваивается значение, возвращаемое из метода bar (), но это значение вообще не используется. int pl = foo(); int p2 = bar(); if (baz()) { return pl; } else { p2 = pl; } return p2; Решение, соответствующее принятым нормам Код из приведенного выше примера можно исправить самыми разными способами в за- висимости от намерений программиста. В представленном здесь решении, соответствующем принятым нормам понятного программирования на Java, переменная р2 оказывается лишней. Вызовы методов bar () и baz () можно было бы удалить, если бы они не давали никаких побочных эффектов. int pl = fоо(); bar(); /* удалить, если у метода Ьаг() отсутствуют побочные эффекты */ baz(); /* удалить, если у метода baz() отсутствуют побочные эффекты */ return pl;
202 Глава 4 Понятность программ Применимость Присутствие пассивного кода может свидетельствовать о логических ошибках, способных привести к непреднамеренному поведению программы. Способы внедрения пассивного кода в программу и его удаления из нее могут оказаться сложными и потребовать немалых усилий. В итоге разрешение проблемы пассивного кода может превратиться в трудоемкий процесс, требующий тщательного анализа. В исключительных случаях пассивный код может сделать программное обеспечение при- годным для последующих изменений. Примером тому служит присутствие ветви с меткой default в операторе switch, несмотря на то, что в нем указаны все возможные варианты выбора ветвей с метками case (демонстрацию этого примера см. в рекомендации 64 “Стре- митесь к логической полноте”)- Допускается также сохранение пассивного кода, который мо- жет понадобиться впоследствии. В подобных случаях такое намерение следует разъяснить в соответствующем комментарии. Наличие кода, не оказывающего никакого воздействия, может свидетельствовать о логи- ческих ошибках, способных привести к неожиданному поведению и уязвимостях в защите программы. Значения, неиспользуемые в коде, также могут свидетельствовать о логических ошибках. Код и значения, не оказывающие никакого воздействия, могут быть обнаружены подходящими средствами статического анализа. Библиография [Fortify 2013] Code Quality: Dead Code [Coverity 2007] Coverity Prevent™ User's Manual (3.3.0) 64. Стремитесь к логической полноте Уязвимости в программном обеспечении могут стать причиной того, что программисту не удастся принять во внимание все возможные состояния данных. Пример кода, не соответствующего принятым нормам (цепочка условных операторов if) В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, не удается проверить условия, при которых значение переменной а не равно ни одному из значений переменных b и с. В данном случае подобное поведение может быть правильным. Но неспособность принять во внимание все значения может привести к логи- ческим ошибкам, если неожиданно допускается другое значение. if (а == Ь) { /* ... */ } else if (а == с) { /* ... */ }
64. Стремитесь к логической полноте 203 Решение, соответствующее принятым нормам (цепочка условных операторов if) В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, организуется явная проверка неожидаемого условия и выполняется соответствующая обработка. if (а == Ь) { /* ... */ } else if (а == с) { /* ... */ } else { /* обработать ошибочное условие */ } Пример кода, не соответствующего принятым нормам (оператор switch) Несмотря на то что в данном примере кода, не соответствующего принятым нормам по- нятного программирования на Java, значение переменной х должно представлять бит (0 или 1), какая-нибудь предыдущая ошибка позволяет допустить совсем иное значение переменной х. Обнаружение и обработка такого недопустимого состояния рано или поздно упростят вы- явление ошибки. switch (х) { case 0: foo(); break; case 1: bar(); break; Решение, соответствующее принятым нормам (оператор switch) В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, предоставляется ветвь оператора switch с меткой default, чтобы обработать все возможные значения типа int. switch (х) { case 0: foo(); break; case 1: bar(); break; default: /* Обработать ошибку */ break; Пример кода, не соответствующего принятым нормам (Zune 30) Данный пример кода, не соответствующего принятым нормам понятного программиро- вания на Java, адаптирован из кода на С, применявшегося в мультимедийном проигрывателе Zune 30 и вызывавшего блокировку многих проигрывателей в полночь по стандартному ти- хоокеанскому времени 30 декабря 2008 года. Этот код содержит незавершенную логику, при- водящую к отказу в обслуживании при преобразовании дат. final static int ORIGIN_YEAR = 1980; /* Количество дней, начиная с 1 января 1980 года */
204 Глава 4 Понятность программ public void convertDays(long days){ int year = ORIGIN_YEAR; /* ... */ while (days > 365) { if (IsLeapYear(year)) { if (days > 366) { days -= 366; year += 1; } } else { days -= 365; year += 1; } } Исходная функция ConvertDays () на С в стандартных процедурах определения истин- ного времени на основе интегральной микросхемы MCI3783 PMIC RTC берет количество дней, начиная с 1 января 1980 года, и рассчитывает правильное количество дней, прошедших после этой исходной даты, а также определяет текущий год. Ошибка в рассматриваемом здесь коде возникает, когда количество дней достигает величины 366, поскольку цикл на этом не завершается. Эта программная ошибка проявилась на 366-й день 2008 года, ставшего первым високосным годом, в котором действовал данный код. Решение, соответствующее принятым нормам (Zune 30) Представленное здесь решение было предложено в статье “A Lesson on Infinite Loops” (На- ставление по бесконечным циклам) Брайанта Задегана (Bryant Zadegan) [Zadegan 2009]. Завер- шение цикла гарантируется благодаря уменьшению количества дней в переменной days на каждом шаге цикла до тех пор, пока условие в цикле while не станет ложным. В последнем случае цикл завершается. Данное решение, соответствующее принятым нормам понятного программирования на Java, представлено лишь в целях демонстрации и поэтому может отли- чаться от конкретного решения, реализованного корпорацией Microsoft. final static int ORIGIN_YEAR = 1980; /* Количество дней, начиная с 1 января 1980 года */ public void convertDays(long days){ int year = ORIGIN_YEAR; /* ... */ int daysThisYear = (IsLeapYear(year) ? 366 : 365); while (days > daysThisYear) { days -= daysThisYear; year += 1; daysThisYear = (IsLeapYear(year) ? 366 : 365); } } Применимость Если не принять во внимание все возможности логического оператора, способные привес- ти к нарушению состояния выполнения программы, то в конечном итоге это может привести к непреднамеренному раскрытию информации или ненормальному завершению программы.
65. Избегайте неоднозначной или вносящей путаницу перегрузки 205 Библиография [Hatton 1995] [Viega 2005] [Zadegan 2009] §2.7.2, "Errors of Omission and Addition" §5.2.17, "Failure to Account for Default Case in Switch" A Lesson on Infinite Loops 65. Избегайте неоднозначной или вносящей путаницу перегрузки Перегрузка допускает объявление методов или конструкторов с одинаковым именем, но разными списками параметров. Компилятор проверяет каждый вызов для перегрузки метода или конструктора, используя объявляемые типы параметров метода, чтобы принять решение, какой именно метод следует вызывать. Но в некоторых случаях может возникнуть недоразу- мение из-за наличия таких относительно новых языковых средств, как автозагрузка и обоб- щения. Более того, методы или конструкторы с однотипными параметрами, но разным поряд- ком объявления, как правило, не отмечаются компиляторами Java. Ошибки могут возник- нуть в том случае, если разработчик не удосужится справиться в документации относительно каждого применения метода или конструктора. Еще одно скрытое препятствие заключается в связывании разной семантики с каждым перезагружаемым методом или конструктором. Определение разной семантики иногда требует разного порядка следования одних и тех же параметров метода, образуя тем самым порочный крут. В качестве примера рассмотрим пе- регружаемый метод getDistance (), один перегружаемый вариант которого возвращает расстояние, преодоленное от исходного пункта, тогда как другой (с переупорядоченными па- раметрами) — расстояние, которое осталось преодолеть до пункта назначения. Те, кто реа- лизуют данный метод, могут неверно понять отличия в его перегружаемых вариантах, если только они не обратятся за справкой к документации по поводу применения каждого из них. Пример кода, не соответствующего принятым нормам (конструктор) Конструкторы нельзя переопределять, а можно лишь перегружать. В данном примере кода, не соответствующего принятым нормам понятного программирования на Java, демонстриру- ется класс Con с тремя перегружаемыми конструкторами. class Con { public Con(int i, String s) { // последовательность инициализации #1 } public Con(String s, int i) { // последовательность инициализации #2 } public Con(Integer i, String s) { // последовательность инициализации #3 } } Если не проявить внимание и аккуратность при передаче аргументов этим конструкторам, то может возникнуть недоразумение, поскольку вызовы этих конструкторов содержат одно и то же количество конкретных однотипных параметров. Перегрузки следует избегать и в
206 Глава 4 « Понятность программ том случае, если перегружаемые конструкторы и методы предоставляют отдельную семантику для формальных параметров одного и того же типа, отличающихся только порядком своего объявления. Решение, соответствующее принятым нормам (конструктор) В представленном здесь решении, соответствующем принятым нормам понятного про- граммирования на Java, перегрузка исключается благодаря объявлению открытых статичес- ких фабричных методов с разными именами вместо открытых конструкторов класса. public static Con createConl(int i, String s) { /* последовательность инициализации #1 */ } public static Con createCon2(String s, int i) { /* последовательность инициализации #2 */ } public static Con createCon3(Integer i, String s) { /* последовательность инициализации #3 */ } Пример кода, не соответствующего принятым нормам (метод) В данном примере кода, не соответствующего принятым нормам понятного программи- рования на Java, класс Ove г Loader содержит экземпляр класса HashMap и перегружаемые методы getData (). В одном таком методе getData () выбирается запись для возврата по ее ключу в отображении. А в остальных случаях запись выбирается на основании конкретного отображаемого значения. class OverLoader extends HashMap<Integer,Integer> { HashMap<Integer,Integer> hm; public OverLoader() { hm = new HashMap<Integer, Integer>(); // записи с номерами социального страхования hm.putd, 111990000); hm.put(2, 222990000); hm.put(3, 333990000); } public String getData(Integer i) { // последовательность перегрузки #1 String s = get(i).toString(); // получить конкретную запись return (s.substring(0, 3) + + s.substring(3, 5) + + s.substring(5, 9)); } public Integer getData(int i) { // последовательность перегрузки #2 return hm.get(i); // получить запись на позиции ’i’ } // проверить, наличие номеров социального страхования ^Override public Integer get(Object data) { // метод SecurityManagerCheck() for (Map.Entry<Integer, Integer> entry : hm.entrySet()) { if (entry.getValue().equals(data)) { return entry.getValue(); // существует
65. Избегайте неоднозначной или вносящей путаницу перегрузки 207 } } return null; } public static void main(String[] args) { OverLoader bo = new OverLoader(); // получить запись по индексу '3’ System.out.printin(bo.getData(3)); // получить запись, содержащую данные ’111990000’ System.out.printin(bo.getData((Integer)111990000)); } } В целях разрешения перегрузки сигнатуры отдельных методов getData () отличаются только статическим типом их формальных параметров. Класс Ove г Loader наследует от клас- са j ava. util. HashMap и перегружает его метод get () для обеспечения функций проверки. Такая реализация может оказаться крайне запутанной для клиента, ожидающего одинакового поведения от обоих методов getData (), независимо от того, был ли указан индекс записи или извлекаемое значение. Несмотря на то что разработчик клиентской программы может в конечном итоге прийти к заключению о подобном поведении, в других случаях (например, с интерфейсом List) оно может оказаться незамеченным. По этому поводу Джошуа Блох (Joshua Bloch) [Bloch 2008] говорит следующее: “В интерфейсе List<E> имеются два перегружаемых метода удаления: remove (Е) и remove (int). До выпуска версии Java 1.5, начиная с которой интерфейс List был сделан обобщенным, в нем имелся метод remove (Object) вместо метода remove (Е), а типы соответствующих параметров (Object и int) радикально отличались. Но бла- годаря появлению обобщений и автоупаковки типы обоих параметров теперь уже не отличаются радикально” Следовательно, программист может и не понять, что из списка был удален не тот элемент. Еще одно затруднение состоит в том, что благодаря автоупаковке ввод определения нового перегружаемого метода может нарушить нормально работавший ранее клиентский код. Это может произойти при вводе нового перегружаемого метода с более конкретным типом пара- метра в прикладной интерфейс API, в прежних версиях методов которого применялись менее конкретные типы параметров. Так, если в прежней версии класса Ove г Loader предоставлялся только один метод getData (Integer), то клиент мог правильно вызвать данный метод, передав ему пара- метр типа int. Результат выбирался бы на основании его значения, поскольку параметр int автоматически упаковывался в объект типа Integer. Следовательно, при вводе метода getData (int) в прикладной код компилятор разрешает все его вызовы с параметром типа int для обращения к новому методу getData (int), изменяя таким образом их семантику, а возможно, и нарушая корректный ранее код. Компилятор действует в подобных случаях совершенно правильно, а конкретное затруднение связано с несовместимым изменением в прикладном интерфейсе API.
208 Глава 4 Понятность программ Решение, соответствующее принятым нормам (метод) Благодаря именованию двух связанных методов по-разному исключается как перегрузка, так и недоразумение, как показано ниже. public Integer getDataBylndex(int i) { // больше не перегружается } public String getDataBy Value(Integer i) { // больше не перегружается } Применимость Неоднозначное и вводящее в заблуждение применение перегрузки может привести к не- ожиданным результатам. Библиография [API 2013] Interface Collection<E> [Bloch 2008] Item 41, "Use Overloading Judiciously"
Глава Ложные представления программистов Рекомендации, приведенные в этой главе, относятся к тем областям программирования на Java, где разработчики нередко делают неоправданные предположения о языке Java и поведе- нии его библиотек или где легко может быть внесена неоднозначность. Пренебрежение этими рекомендациями может привести к противоречивым результатам выполнения программ. В частности, приведенные в этой главе рекомендации направлены на разрешение следую- щих вопросов. 1. Ложные представления о языковых средствах и прикладных интерфейсах Java API. 2. Предположения и программы, чреватые неоднозначностью. 3. Ситуации, в которых программист намеревался достичь одного результата, а добился совсем другого. 66. Не принимайте на веру, что объявление изменчивой ссылки гарантирует надежную публикацию членов объекта, доступного по этой ссылке В §8.3.1.4 “Изменчивые поля” спецификации языка программирования Java (JLS) [JLS 2013] говорится следующее. “Поле может быть объявлено как изменчивое (volatile), ив этом случае модель па- мяти языка Java гарантирует, что во всех потоках будет доступно согласованное значе- ние для переменной (см. §17.4)”
210 Глава 5 Ложные представления программистов Гарантия такой надежной публикации распространяется на поля примитивных типов и ссылки на объекты. Программисты обычно пользуются неточной терминологией, говоря об “объектах-членах” В целях такой гарантии доступности конкретный член доступен по ссылке на объект. А объекты, доступные по ссылкам на изменчивые объекты (так называемые объек- ты ссылки^ находятся вне действия гарантии данной надежной публикации. Следовательно, объявления изменчивой ссылки на объект недостаточно для гарантии того, чтобы изменения в членах объекта ссылки публиковались в других потоках. В одном потоке, возможно, и не удастся наблюдать последнюю запись из другого потока в поле элемента данных. Более того, когда объект ссылки является изменяемым, но не потокобезопасным, в других потоках может быть доступен частично или полностью построенный объект, находящийся (временно) в неопределенном состоянии [Goetz 2007]. Но если объект ссылки является не- изменяемым, то объявления изменчивой ссылки оказывается достаточно для гарантии на- дежной публикации членов объекта ссылки. Программисты не могут пользоваться ключе- вым словом volatile для гарантии безопасной публикации изменяемых объектов. Поэтому пользование ключевым словом volatile может только гарантировать надежную публика- цию полей примитивных типов данных, ссылок на объекты или полей объектов ссылок на неизменяемые объекты. Путаница изменчивого объекта с изменчивостью объектов-членов сродни ошибке, описанной в рекомендации 73 “Не путайте неизменяемость ссылки и доступ- ного по ссылке объекта”. Пример кода, не соответствующего принятым нормам (массивы) В данном примере кода, не соответствующего принятым нормам корректного программи- рования на Java, объявляется изменчивая ссылка на объект массива. final class Foo { private volatile int[] arr = new int[20]; public int getFirstO { return arr[0]; } public void setFirst(int n) { arr[0] = n; } // ... } Значения, присваиваемые элементам массива в одном потоке, например, в результате вы- зова метода setFirst (), могут оказаться недоступными в другом потоке, вызывающем ме- тод getFirst (), потому что ключевое слово volatile гарантирует надежную публикацию только для ссылки на массив. Но оно не дает никаких гарантий относительно конкретных данных, содержащихся в массиве. Подобное затруднение возникает, когда потоку, вызывающему метод setFirst (), и по- току, вызывающему метод getFirst (), недостает заранее установленного отношения. Такое отношение возникает между потоком, записывающим данные в изменчивую переменную, и потоком, читающим впоследствии данные из этой переменной. Но каждый из методов setFirst () и getFirst () читает данные из изменчивой переменной (в данном случае — изменчивой ссылки на массив), и поэтому ни один из них не записывает данные в изменчи- вую переменную.
бб. Не принимайте на веру, что объявление изменчивой ссылки гарантирует... 211 Решение, соответствующее принятым нормам (класс AtomicIntegerArray) Для гарантии атомарности операций записи в элементы массива и доступности ре- зультирующих значений в других потоках, в представленном здесь решении, соответс- твующем принятым нормам корректного программирования на Java, применяется класс AtomicIntegerArray, определенный в пакете j ava. util. concurrent. atomic. Этот класс гарантирует заранее установленное отношение между потоком, вызывающим метод atomicArray. set (), и потоком, вызывающим впоследствии метод atomicArray.get (). final class Foo { private final AtomicIntegerArray atomicArray = new AtomicIntegerArray(20); public int getFirstO { return atomicArray.get(0); } public void setFirst(int n) { atomicArray.set(0, 10); } // ... } Решение, соответствующее принятым нормам (синхронизация) Для гарантии доступности данных методы доступа должны синхронизировать его при выполнении операций над неизменчивыми элементами массива независимо от того, явля- ется ли ссылка на него изменчивой или неизменчивой. Следует иметь в виду, что благодаря данному решению код оказывается потокобезопасным, даже если ссылка на массив не явля- ется изменчивой. final class Foo { private int[] arr = new int[20]; public synchronized int getFirstO { return arr[0]; } public synchronized void setFirst(int n) { arr[0] = n; } } Синхронизация заранее устанавливает отношение между потоками, синхронизируя их по одной и той же блокировке. В данном случае поток, вызывающий метод setFirst (), и поток, вызывающий впоследствии метод getFirst (), синхронизируются для одного и того же экземпляра объекта. И благодаря этому гарантируется надежная публикация членов этого объекта.
212 Глава 5 Ложные представления программистов Пример кода, не соответствующего принятым нормам (изменяемый объект) В данном примере кода, не соответствующего принятым нормам корректного программи- рования на Java, объявляется изменчивое поле экземпляра объекта типа Мар. И это экземпляр объекта, изменяемого благодаря его методу put (). final class Foo { private volatile Map<String, String> map; public Foo() { map = new HashMap<String, String>(); // загрузить ряд полезных значений в отображение } public String get(String s) { return map.get(s); } public void put(String key, String value) { // проверить достоверность значений перед их вводом if (lvalue.matches("[\\w]*")) { throw new IllegalArgumentExceptionO; } map.put(key, value); } Чередование вызовов методов get () и put () может привести к извлечению внутренне несогласованных значений из объекта типа Мар, поскольку метод put () видоизменяет его состояние. А объявления изменчивой ссылки на объект оказывается недостаточно для исклю- чения подобной гонки данных. Пример кода, не соответствующего принятым нормам (изменчивое чтение и синхронизированная запись) В данном примере кода, не соответствующего принятым нормам корректного программи- рования на Java, предпринимается попытка воспользоваться способом изменчивого чтения и синхронизированной записи, описанным в серии статьей под общим названием “Java Theory and Practice” (Теория и практика программирования на Java) [Goetz 2007]. Поле map объявля- ется изменчивым для синхронизации операций чтения и записи в него. Метод put () также синхронизируется для обеспечения атомарности его выполнения. final class Foo { private volatile Map<String, String> map; public Foo() { map = new HashMap<String, String>(); // загрузить ряд полезных значений в отображение } public String get(String s) { return map.get(s);
66. Не принимайте на веру, что объявление изменчивой ссылки гарантирует... 213 public synchronized void put(String key, String value) { // проверить достоверность значений перед их вводом if (lvalue.matches("[\\w]*")) { throw new IllegalArgumentException(); } map.put(key, value); } } В способе изменчивого чтения и синхронизированной записи синхронизация применяет- ся для сохранения атомарности составных операций (например, приращения) и обеспечивает ускоренный доступ к данным для их атомарного чтения. Но такой способ не годится для из- меняемых объектов, поскольку гарантия надежной публикации, обеспечиваемая ключевым словом volatile, распространяется только на само поле (значение примитивного типа или ссылку на объект). Под такую гарантию не подпадает объект ссылки, как, впрочем, и его чле- ны. По существу, записи и последующему чтению данных из отображения недостает заранее установленного отношения. Данный способ обсуждается также в рекомендации “VNA02-J. Обеспечивайте атомарность операций над разделяемыми переменными” из стандарта The CERT* Oracle* Secure Coding Standard for Java" [Long 2012]. Решение, соответствующее принятым нормам (синхронизация) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, синхронизация применяется для гарантии доступности данных. final class Foo { private final Map<String, String> map; public Foo() { map = new HashMap<String, String>(); // загрузить ряд полезных значений в отображение } public synchronized String get(String s) { return map.get(s); } public synchronized void put(String key, String value) { // проверить достоверность значений перед их вводом if (lvalue.matches("[\\w]*")) { throw new IllegalArgumentException(); } map.put(key, value); } } Объявлять поле map изменчивым совсем не обязательно, поскольку методы доступа син- хронизированы. Это поле объявляется конечным (final) для предотвращения публикации его ссылки, когда объект ссылки находится в частично инициализированном состоянии (под- робнее об этом см. рекомендацию “TSM03-J. Не публикуйте частично инициализированные объекты” из стандарта The CERT* Oracle* Secure Coding Standard for Java" [Long 2012]).
214 Глава 5 Ложные представления программистов Пример кода, не соответствующего принятым нормам (изменяемый подобъект) В данном примере кода, не соответствующего принятым нормам корректного программи- рования на Java, в изменчивом поле format сохраняется ссылка на изменяемый объект типа j ava. text. DateFormat. А поскольку объект типа DateFormat не является потокобезопас- ным [API 2013], значение для объекта типа Date, возвращаемое методом parse (), может и не соответствовать аргументу str. Решение, соответствующее принятым нормам (защитное копирование экземпляра при каждом вызове метода) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, создается и возвращается новый экземпляр класса DateFormat при каждом вызове метода parse () [API 2013]. final class DateHandler { public static java.util.Date parse(String str) throws ParseException { return DateFormat.getDatelnstance( DateFormat.MEDIUM).parse(str); } } Решение, соответствующее принятым нормам (синхронизация) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, объект типа DateHandler становится потокобезопасным благодаря синхронизации операторов в методе parse () [API 2013]. final class DateHandler { private static DateFormat format = DateFormat.getDatelnstance(DateFormat.MEDIUM); public static java.util.Date parse(String str) throws ParseException { synchronized (format) { return format.parse(str); } } } Решение, соответствующее принятым нормам (сохранение объекта типа ThreadLocal) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, объект типа ThreadLocal применяется для создания отдельного эк- земпляра объекта типа DateFormat в каждом потоке. final class DateHandler { private static final ThreadLocal<DateFormat> format = new ThreadLocal<DateFormat>() { (^Override protected DateFormat initialvalue () {
67. Не принимайте на веру, что методы... 215 return DateFormat.getDatelnstance(DateFormat.MEDIUM); } }; // ... Применимость Неверное предположение, что объявление поля изменчивым гарантирует надежную пуб- ликацию членов доступного по ссылке объекта, может привести к тому, что в потоках будут наблюдаться устаревшие и несогласованные значения. Формально строгая неизменяемость объекта ссылки оказывается более строгим условием, чем, по существу, требуется для надеж- ной публикации. Когда же объект ссылки требуется специально определить потокобезопас- ным, то поле, содержащее ссылку на него, может быть объявлено изменчивым. Но такой под- ход к использованию ключевого слова volatile снижает степень сопровождаемости кода, и поэтому его следует всячески избегать. Библиография [API 2013] [Goetz 2007] [JLS 2013] [Long 2012] Class DateFormat Pattern 2, "One-Time Safe Publication" §8.3.1.4, "volatile Fields" OBJ05-J. Defensively copy private mutable class members before returning their references TSM03-J. Do not publish partially initialized objects VNA02-J. Ensure that compound operations on shared variables are atomic [Miller 2009] "Mutable Statics" 67. Не принимайте на веру, что методы sleep (), yield () или getState () предоставляют семантику синхронизации В §17.3 “Ожидание и выдача” спецификации JLS [JLS 2013] говорится следующее. “Следует иметь в виду, что ни у метода Thread. sleep (), ни у метода Thread. yield () нет никакой семантики синхронизации. В частности, компилятор не должен ни запи- сывать данные, кешированные в регистрах, в разделяемую память перед вызовом ме- тода Thread. sleep () или Thread. yield (), ни перезагружать данные, кеширован- ные в регистрах, после вызова метода Thread, sleep () или Thread. yield () ”. Код, надежность параллельного выполнения которого основывается на приостановке по- токов или выдаче данных для тех процессов, где выполняется любая из перечисленных ниже операций, является некорректным, а следовательно, несогласованным. Вывод данных, кешированных в регистрах. Перегрузка любых значений. Предоставление заранее установленных отношений при возобновлении выполнения.
216 Глава 5 Ложные представления программистов Поэтому в программах должна быть обеспечена надлежащая семантика синхронизации взаимодействия потоков, заранее установленного отношения и надежной публикации. Пример кода, не соответствующего принятым нормам (метод sleep ()) В данном примере кода, не соответствующего принятым нормам корректного програм- мирования на Java, предпринимается попытка воспользоваться неизменчивым членом done логического типа в качестве признака завершения исполняемого потока. Логическое зна- чение true этого члена устанавливается в отдельном потоке в результате вызова метода shutdown(). final class ControlledStop implements Runnable { private boolean done = false; (^Override public void run() { while (’done) { try { Thread.sleep(1000); } catch (InterruptedException e) { // сбросить прерываемое состояние Thread.currentThread().interrupt(); } } } public void shutdown() { this.done = true; } } В данном случае компилятор может свободно прочитать данные из поля this .done один раз и повторно использовать кешированное значение на каждом шаге выполнения цикла. Следовательно, цикл while может вообще не завершиться, даже если метод shutdown () вы- зывается из другого потока для изменения значения в поле this.done [JLS 2013]. Подобная ошибка может возникнуть в результате неверного предположения программиста, что вызов метода Thread, sleep () приводит к перезагрузке кешированных данных. Решение, соответствующее принятым нормам (изменчивый признак) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, поле признака done объявляется изменчивым (volatile), чтобы обновления его значения были доступны в нескольких потоках. Ключевое слово volatile заранее устанавливает отношение между текущим потоком и любым другим потоком, в кото- ром задается значение в поле признака done. final class ControlledStop implements Runnable { private volatile boolean done = false; @Override public void run() { //... } // ... }
67. Не принимайте на веру, что методы... 217 Решение, соответствующее принятым нормам (метод Thread. interrupt ()) Лучшее решение для методов, вызывающих метод sleep (), состоит в прерывании потока, приводящем к тому, что поток, находящийся в состоянии ожидания, сразу же активизируется и обрабатывает прерывание. final class ControlledStop implements Runnable { @Override public void run() { // зарегистрировать текущий поток, чтобы его можно было // прерывать в других потоках myThread = currentThread(); while (!Thread.interrupted()) { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public void shutdown(Thread th) { th.interrupt(); } } Следует, однако, иметь в виду, что в прерывающем потоке должно быть известно, какой именно поток следует прерывать. Логика отслеживания такого отношения в данном решении отсутствует. Пример кода, не соответствующего принятым нормам (Thread. getState ()) В данном примере кода, не соответствующего принятым нормам корректного програм- мирования на Java, применяется метод doSomething (), запускающий поток на исполнение. Прерывание в этом потоке поддерживается благодаря проверке соответствующего признака flag и ожиданию уведомления о прерывании. В методе stop () проверяется, блокирован ли поток по ожиданию. Если он блокирован, то устанавливается логическое значение true при- знака flag, а поток уведомляется о том, что он может быть прерван. public class Waiter { private Thread thread; private boolean flag; private final Object lock = new Object(); public void doSomething() { thread = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { while (’flag) { try { lock.wait(); // ...
218 Глава 5 Ложные представления программистов } catch (InterruptedException е) { // направить обработчику исключений } } } } }); thread.start(); public boolean stop() { if (thread != null) { if (thread.getState() == Thread.State.WAITING) { synchronized (lock) { flag = true; lock.notifyAll(); } return true; } } return false; } } К сожалению, метод Thread. getState () некорректно используется в методе stop (), чтобы проверить, заблокирован ли поток и не завершился ли он прежде отправки уведомле- ния об этом событии. Метод Thread.getState () не годится для управления синхрониза- цией и, в частности, для проверки блокировки потока по ожиданию. В виртуальных машинах Java (JVM) разрешается реализовывать блокировку, используя ожидание в состоянии заня- тости. Следовательно, поток можно заблокировать, не переводя его в состояние ожидания (WAITING) или же состояние ожидания с ограничением по времени (TIMED_WAITING) [Goetz 2006]. А поскольку поток может вообще не войти в состояние WAITING, то методу stop (), возможно, и не удастся остановить его. Если методы doSomething () и stop () вызываются из разных потоков, то методу stop () может оказаться недоступным инициализированный поток, даже несмотря на то, что метод doSomething () был вызван раньше, если только между вызовами обоих методов не сущест- вует заранее установленное отношение. Если же оба метода вызываются из одного и того же потока, то отношение между ними автоматически устанавливается заранее, а следовательно, они не сталкиваются с подобным затруднением. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, исключается проверка нахождения потока в состоянии WAITING. Та- кая проверка не нужна потому, что вызов метода not if yAll () оказывает воздействие только на потоки, заблокированные в результате вызова метода wait (). public class Waiter { // . . . private Thread thread; private volatile boolean flag; private final Object lock = new Object();
68. Не принимайте на веру, что оператор вычисления остатка... 219 public boolean stop() { if (thread != null) { synchronized (lock) { flag = true; lock.notifyAll(); } return true; } return false; Применимость Если полагаться на методы sleep (), yield () и get State () из класса Thread для уп- равления синхронизацией потоков, то в конечном итоге это может привести к неожиданному поведению кода. Библиография [Goetz 2006] [JLS 2013] §17.3, “Sleep and Yield" 68. He принимайте на веру, что оператор вычисления остатка всегда возвращает неотрицательный результат для целочисленных операндов В §15.17.3 “Оператор вычисления остатка %” спецификации JLS [JLS 2013] говорится сле- дующее. “Операция вычисления остатка над целочисленными операндами после продвижения к двоичному числовому типу данных (§5.6.2) дает такое результирующее значение, что выражение (а/b) *Ь+ (а%Ь) оказывается равным а. Такая идентичность сохраняется даже в особом случае, когда делимое оказывается отрицательным целым значением максимально возможной для данного типа величины, а делитель равен -1 (т.е. остаток от деления равен нулю). Из этого правила следует, что результат операции вычисления остатка может быть отрицательным только в том случае, если отрицательным оказыва- ется делимое. Он может быть положительным только в том случае, если делимое ока- зывается положительным. Более того, величина результата всегда оказывается меньше величины делителя”. Результат операции вычисления остатка имеет тот же самый знак, что и у делимого (т.е. первого операнда выражения), как показано ниже. В итоге код, зависящий от опера- ции вычисления остатка, чтобы всегда возвращать положительный результат, оказывается ошибочным. 5 % 3 дает 2 5 % (-3) дает 2 (-5) % 3 дает -2 (-5) % (-3) дает -2
220 Глава 5 Ложные представления программистов Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам корректного про- граммирования на Java, целочисленная переменная hashKey служит в качестве ин- декса хеш-массива. Отрицательный хеш-ключ дает отрицательный результат опера- ции вычисления остатка, а в итоге метод lookup () генерирует исключение типа j ava.lang.ArraylndexOutOfBoundsException. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, вызывается метод imod (), всегда возвращающий положительный остаток. // метод imod() дает неотрицательный результата private int SIZE = 16; public int[] hash = new int[SIZE]; private int imod(int i, int j) { int temp = i % j; // унарный минус приносит успех без переполнения, т.к. // переменная temp не может иметь значение Integer.MIN_VALUE return (temp < 0) ? -temp : temp; } public int lookup(int hashKey) { return hash[imod(hashKey, SIZE)]; } Применимость Неверное допущение о положительном результате операции вычисления остатка может привести к ошибочному коду. Библиография [JLS 2013] §15.17.3, "Remainder Operator %" 69. Не путайте равенство абстрактных объектов с равенством ссылок В языке Java определяются операторы = и ? = для проверки на равенство ссылок, а метод equals (), определенный в классе Object и его подклассах, служит для проверки на равенс- тво объектов. Неопытные программисты нередко путают назначение оператора = и метода Object.equals (). Такая путаница нередко становится очевидной в контексте обработки объектов типа String. Как правило, метод Object.equals () служит для того, чтобы проверить, эквивалентно ли содержимое двух объектов, а операторы равенства = и ? = — чтобы проверить, делаются ли две ссылки на один и тот же объект. Последняя проверка называется ссылочным равенс- твом. В тех классах, где требуется переопределение исходной реализации метода equals (), необходимо уделить внимание и методу hashCode () (см. рекомендацию “MET09-J. В тех
69. Не путайте равенство абстрактных объектов с равенством ссылок 221 классах, где определяется метод equals (), должен быть также определен метод hashCode () ” из стандарта The CERT9 Oracle9 Secure Coding Standard for Java™ [Long 2012]). Данные числовых упакованных типов (например, Byte, Character, Short, Integer, Long, Float и Double) должны также сравниваться с помощью метода Object .equals (), а не оператора =. И если ссылочное равенство может подойти для сравнения значений типа Integer в пределах от -128 до 127, то оно может оказаться непригодным, если любой из сравниваемых операндов оказывается вне этих пределов. Числовые операторы отношения, кроме операторов равенства (например, операторы <, <=, > и >=), могут надежно служить для сравнения упакованных данных примитивных типов (см. также рекомендацию “EXP03-J. Не пользуйтесь операторами равенства при сравнении значений упакованных примитивных типов” из стандарта The CERT9 Oracle9 Secure Coding Standard for fava™ [Long 2012]). Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам корректного програм- мирования на Java, объявляются два разных объекта типа String, содержащих одно и то же значение. public class Stringcomparison { public static void main(String[] args) { String strl = new String("one"); String str2 = new String("one"); System.out.printin(strl == str2); // выводит строку "false” } } Вычисление оператора ссылочного равенства = дает логическое значение true только в том случае, если сравниваемые ссылки делаются на один и тот же базовый объект. В данном примере ссылки неравны, поскольку они делаются на разные объекты. Решение, соответствующее принятым нормам (метод Object. equals ()) В представленном здесь решении, соответствующем принятым нормам корректно- го программирования на Java, для сравнения строковых значений применяется метод Object.equals(). public class Stringcomparison { public static void main(String[] args) { String strl = new String("one"); String str2 = new String("one"); System.out.printin(strl.equals(str2)); // выводит строку "true" } } Решение, соответствующее принятым нормам (метод String. intern ()) Ссылочное равенство действует аналогично равенству абстрактных объектов, когда оно используется для сравнения двух символьных строк, получаемых в результате вызова мето- да String, intern (). В представленном здесь решении, соответствующем принятым нор- мам корректного программирования на Java, применяется метод String. intern (), что
222 Глава 5 Ложные представления программистов позволяет быстро сравнивать символьные строки, когда в памяти требуется хранить только одну копию символьной строки one. public class Stringcomparison { public static void main(String[] args) { String strl = new String (’’one") ; String str2 = new String("one"); strl = strl.intern(); str2 = str2.intern(); System.out.printin(strl == str2); // выводит строку "true" } } Применение метода String, intern () следует оставить на те случаи, когда разбиение символьных строк на лексемы дает существенное повышение производительности или уп- рощение кода. Примерами тому служат программы, задействованные в инструменталь- ных средствах обработки текста на естественных языках и разбиения на лексемы входных данных подобно компиляторам. А в большинстве других программ производительность и удобочитаемость зачастую повышаются с помощью кода, в котором применяется метод Object.equals () и отсутствует любая зависимость от ссылочного равенства. В спецификации JLS предоставляется немного гарантий реализации метода String. intern (). В частности, описанные ниже. Затраты на реализацию метода String. intern () возрастают по мере увеличения ко- личества интернированных символьных строк. А производительность должна быть не хуже, чем О (n log п), но в спецификации JLS конкретной гарантии такой произво- дительности не дается. В прежних реализациях виртуальной машины Java (JVM) интернированные строки становились практически “бессмертными”, т.е. они освобождались от “сборки мусора”. Это могло вызвать осложнения в тех случаях, когда интернировались большие коли- чества символьных строк. А в последующих реализациях JVM “сборка мусора” может быть произведена в оперативной памяти, выделяемой для интернированных символь- ных строк, на которые больше не делаются ссылки. Но в спецификации JLS подобное поведение не определяется. В виртуальной машине JVM до версии Java 1.7 оперативная память для интерниро- ванных символьных строк выделялась в области permgen, которая, как правило, оказывалась намного меньше, чем остальная динамическая область памяти. Следо- вательно, интернирование большого количества символьных строк могло привести к исчерпанию оперативной памяти. Во многих реализациях виртуальной машины JVM, начиная с версии Java 1.7, для интернированных символьных строк выделяется дина- мическая область памяти (так называемая “куча”), что позволяет снять данное огра- ничение. Но опять же подробности выделения оперативной памяти для этих целей в спецификации JLS не указываются, а следовательно, отдельные реализации могут заметно отличаться. Интернирование символьных строк может также применяться в тех программах, где при- нимаются повторно возникающие символьные строки. Благодаря этому повышается произво- дительность операций сравнения символьных строк и сокращается потребление оперативной памяти.
70. Ясно различайте поразрядные и логические операторы 223 Когда требуется каноническое представление объектов, то для этой цели, возможно, благоразумнее воспользоваться специальным средством, построенным на основе класса ConcurrentHashMap. Подробнее об этом см. п. 69 в книге Effective Java™, Second Edition Джо- шуа Блоха [Bloch 2008]. Применимость Путаница ссылочного равенства с равенством объектов может привести к неожиданным результатам. Применение ссылочного равенства вместо равенства объектов допускается толь- ко в тех случаях, когда определение классов гарантирует существование хотя бы одного эк- земпляра для каждого возможного значения объекта. А применение статических фабричных методов вместо открытых конструкторов упрощает управление экземплярами, и это делается способом активизации ключей. Другой способ состоит в употреблении перечислимого типа. А для того чтобы выяснить, делаются ли две ссылки на один и тот же объект, следует приме- нять ссылочное равенство. Библиография [Bloch 2008] [FindBugs 2008] [JLS 2013] [Long 2012] Item 69, "Prefer Concurrency Utilities to wait and notify" ES, "Comparison of String Objects Using = or !=" §3.10.5, "String Literals" §5.6.2, "Binary Numeric Promotion" EXP03-J. Do not use the equality operators when comparing values of boxed primitives MET09-J. Classes that define an equals () method must also define a hashCode () method 70. Ясно различайте поразрядные и логические операторы Логические операторы И и ИЛИ (операторам && и | | соответственно) имеют укорочен- ный характер. Это означает, что второй операнд таких операторов вычисляется лишь в том случае, если результат логической операции нельзя вывести из вычисления только первого операнда. Следовательно, в тех случаях, когда результат логической операции можно вывести из вычисления только первого операнда, второй операнд остается невычисленным. При этом никаких побочных эффектов не возникает. Укороченный характер не присущ поразрядным логическим операциям И и ИЛИ (опе- раторам & и | соответственно). Подобно большинству других операторов в Java, в этих ло- гических операторах вычисляются оба операнда. Они возвращают то же самое логическое значение, что и логические операторы && и | | соответственно, но могут иметь другие общие эффекты в зависимости от наличия побочных эффектов во втором операнде. Следовательно, логический оператор & и && может быть использован при выполнении бу- левых логических операций. Но в одних случаях более предпочтительным оказывается укоро- ченный характер подобных операций, тогда как в других случаях — их укороченный характер приводит к едва заметным программным ошибкам.
224 Глава 5 Ложные представления программистов Пример кода, не соответствующего принятым нормам (неверное употребление логического оператора &) В данном примере кода, не соответствующего принятым нормам корректного програм- мирования на Java и взятого из первоисточника [Flanagan 2005], имеются две переменные с неизвестными значениями. Поэтому в коде должны быть сначала проверены хранящиеся в них данные, а затем достоверность индекса массива array [ i ]. int array[]; // может быть пустым int i; // может быть недействительным индексом массива if (array != null & i >= 0 & i < array.length & array[i] >= 0) { // использовать массив } else { // обработать ошибку } Приведенный выше код может оказаться неработоспособным в результате тех оши- бок, которые он пытается предотвратить. Когда массив оказывается пустым (NULL) или имеет недействительный индекс, ссылка на такой массив или обращение к нему по индек- су array [i] приведет к генерированию исключения типа NullPointerException или ArraylndexOutOfBoundsException. Появление исключения объясняется тем, что в логи- ческом операторе & не удается предотвратить вычисление его правого операнда, даже если вычисление его левого операнда подтверждает, что правый операнд неуместен, т.е. не имеет никакого значения. Решение, соответствующее принятым нормам (употребление логического оператора &&) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, упомянутый выше недостаток отчасти устраняется употреблением логического оператора &&, в котором вычисление условного выражения прекращается сразу, как только любое из условий оказывается ложным. И благодаря этому предотвращается ис- ключение, возникающее во время выполнения. int array[]; // может быть пустым int i; // может быть недействительным индексом массива if (array != null && i >= 0 && i < array.length && array[i] >= 0) { // обработать массив } else { // обработать ошибку } Решение, соответствующее принятым нормам (вложенные условные операторы if) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, для достижения нужного эффекта применяется несколько условных операторов if. int array[]; // может быть пустым int i; // может быть действительным индексом массива if (array != null) {
70. Ясно различайте поразрядные и логические операторы 225 if (i >= 0 && i < array.length) { if (array[i] >= 0) { // использовать массив } else { // обработать ошибку } } else { // обработать ошибку } } else { // обработать ошибку Несмотря на правильность рассматриваемого здесь решения, оно оказывается более мно- гословным и громоздким, что может затруднить сопровождение кода. Тем не менее такое ре- шение является более предпочтительным, когда код обработки ошибок оказывается другим в каждом возможном случае возникновения сбоев. Пример кода, не соответствующего принятым нормам (неверное употребление логического оператора &&) В данном примере кода, не соответствующего принятым нормам корректного программи- рования на Java, демонстрируется сравнение двух массивов на совпадение в заданных преде- лах изменения их элементов. В частности, индексы il и i2 относятся к массивам arrayl и аггау2 соответственно, а переменные endl и end2 являются индексами конечных пределов, проверяемых в обоих массивах на совпадение. if (endl >= 0 & i2 >= 0) { int begin1 = il; int begin2 = i2; while (++il < arrayl.length && ++i2 < array2.length && arrayl[il] == array2[i2]) { // массивы до сих пор совпадают } int endl = il; int end2 = i2; assert endl - beginl == end2 - begin2; Недостаток приведенного выше кода состоит в том, что если не удовлетворяется первое условие в цикле while, то второе условие не выполняется. Это означает, что, как только ин- декс il достигнет длины массива arrayl. length, цикл завершится после приращения ин- декса il. Следовательно, явные пределы в массиве arrayl оказываются больше, чем в масси- ве аггау2, и поэтому утверждение, завершающее данный код, не соблюдается. Решение, соответствующее принятым нормам (употребление логического оператора &) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, упомянутый выше недостаток отчасти устраняется благоразумным употреблением логического оператора &. Этим гарантируется приращение обоих индексов il и i2 независимо от результата вычисления первого условия в цикле while.
226 Глава 5 Ложные представления программистов public void exampleFuntion() { while (++il < arrayl.length & // не оператор && ++i2 < array2.length && arrayl[il] == array2[i2]){ // сделать что-нибудь Применимость Если не уяснить должным образом поведение поразрядных и логических операторов, то в конечном итоге это может привести к непреднамеренному поведению программы. Библиография [Flanagan 2005] [JLS 2013] §2.5.6., "Boolean Operators" §15.23, "Conditional-And Operator &&" §15.24,"Conditional-OrOperator | 71. Правильно интерпретируйте управляющие символы при загрузке строк Во многих классах допускается включение управляющих последовательностей символов в строковые литералы. Примерами тому служат класс java.util. regex. Pattern, а также классы, поддерживающие операции с данными, представленными в формате XML и получа- емыми по запросам SQL к базе данных, с помощью методов со строковыми аргументами. В §3.10.6 “Управляющие последовательности символов в строковых литералах” спецификации JLS по этому поводу говорится следующее. “Управляющие последовательности символов в строках позволяют представить неко- торые неграфические символы, а также знаки одиночных, двойных кавычек и обрат- ной косой черты в символьных (§3.10.4) и строковых (§3.10.5) литералах”. Для правильного употребления управляющих последовательностей символов в строковых литералах требуется ясное представление о том, каким образом такие последовательности интерпретируются сначала компилятором Java, а затем процессором, например механизмом SQL. В некоторых случаях управляющие последовательности символов (в частности, после- довательности символов \t, \п, \г) могут потребоваться в операторах SQL, например, при сохранении необработанного текста в базе данных. Когда же операторы SQL обозначаются в Java строковыми литералами, то каждой управляющей последовательности должен предшест- вовать знак обратной косой черты для правильной ее интерпретации. В качестве другого примера рассмотрим класс Pattern, применяемый при решении обыч- ных задач вычисления выражений. Строковый литерал, употребляемый для сопоставления с шаблоном, компилируется в экземпляр типа Pattern. Если же сопоставляемый шаблон содер- жит последовательность символов, аналогичную одной из управляющих последовательностей символов (например, символы ”\” и "п"), то компилятор интерпретирует часть символьной строки как управляющую последовательность символов в Java, преобразовав ее непосредс- твенно в символ новой строки. А для того чтобы ввести управляющую последовательность
71. Правильно интерпретируйте управляющие символы при загрузке строк 227 символов "\п" для перехода на новую строку вместо литерала с символом новой строки, эту последовательность следует предварить дополнительным знаком обратной косой черты, что- бы компилятор Java не заменил ее символом новой строки. В итоге получится приведенная ниже символьная строка, правильно обозначающая в шаблоне управляющую последователь- ность для перехода на новую строку: \\п В целом конкретному управляющему символу в общей форме \Х соответствует следующее его представление в Java: \\х Пример кода, не соответствующего принятым нормам (строковый литерал) В данном примере кода, не соответствующего принятым нормам корректного программи- рования на Java, определяется метод splitwords (), в котором обнаруживается совпадение входной последовательности символов со строковым литералом (WORDS). При этом предпо- лагается, что строковый литерал WORDS должен содержать управляющую последовательность символов для совпадения с границей слова. Но компилятор интерпретирует строковый лите- рал "\Ь" как управляющую последовательность в Java, и поэтому символьная строка WORDS молча компилируется в регулярное выражение, проверяющее наличие одного символа обрат- ной косой черты. public class Splitter { // интерпретируется как знак обратной косой черты //не удается произвести разделение строк по границам слов private final String WORDS = "\b”; public String[] splitwords(String input){ Pattern pattern = Pattern.compile(WORDS); String[] input_array = pattern.split(input); return input_array; } } Решение, соответствующее принятым нормам (строковый литерал) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, демонстрируется правильное экранирование значения строкового литерала WORDS. В итоге получается регулярное выражение, предназначенное для разделения строк по границам слов. public class Splitter { // интерпретируется как два символа ’\’ и 'Ь* // правильно разделяет строки по границам слов private final String WORDS = "\\b"; public String[] split(String input){ Pattern pattern = Pattern.compile(WORDS); String[] input_array = pattern.split(input); return input_array; } }
228 Глава 5 Ложные представления программистов Пример кода, не соответствующего принятым нормам (строковое свойство) В данном примере кода, не соответствующего принятым нормам корректного программи- рования на Java, применяется тот же самый метод splitwords (). Но на этот раз символьная строка WORDS загружается из внешнего файла свойств. public class Splitter { private final String WORDS; public Splitter() throws lOException { Properties properties = new Properties(); properties.load(new Fileinputstream("splitter.properties")); WORDS = properties.getProperty("WORDS"); } public String[] split(String input){ Pattern pattern = Pattern.compile(WORDS); String[] input_array = pattern.split(input); return input_array; } } Но свойство WORD опять же неверно указано в файле свойств как последовательность сим- волов \Ь: WORDS=\b Это свойство читается методом Properties. load () как единственный символ Ь, в ре- зультате чего метод split () разделяет символьные строки по букве Ь в качестве границы слов. И хотя символьная строка интерпретируется иначе, чем строковый литерал, как в упо- мянутом выше примере кода, не соответствующего принятым нормам, тем не менее такая интерпретация оказывается неверной. Решение, соответствующее принятым нормам (строковое свойство) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, демонстрируется правильное экранирование значения строкового свойства WORDS: WORDS=\\b Применимость Неправильное употребление управляющих символов в строке может привести к неверной интерпретации, а возможно, и порче данных. Библиография [API 2013] Class Pattern, "Backslashes, Escapes, and Quoting" Package java.sql [JLS 2013] §3.10.6, "Escape Sequences for Character and String Literals"
72. Не пользуйтесь перегружаемыми методами для динамического различения... 229 72. Не пользуйтесь перегружаемыми методами для динамического различения типов данных В Java поддерживается перегрузка методов, и поэтому различаются методы с разными сигнатурами. Следовательно, в некоторых описаниях методов в классе может употребляться одно и то же имя, если их параметры отличаются. При перегрузке метод, вызываемый во вре- мя выполнения, определяется на стадии компиляции. Следовательно, перегружаемый метод, связанный со статическим типом объекта, вызывается даже в том случае, если тип данных отличается при каждом вызове данного метода во время выполнения. Для того чтобы программа стала понятнее, старайтесь не вносить неоднозначность при перегрузке методов (см. также рекомендацию 65 “Избегайте неоднозначной или вносящей путаницу перегрузки”). Кроме того, старайтесь пользоваться перегружаемыми методами как можно более экономно [Tutorials 2013]. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам корректного про- граммирования на Java, предпринимается попытка воспользоваться перегружаемым ме- тодом display () для выполнения разных действий, в зависимости от того, передается ли этому методу списочный массив типа ArrayList<Integer> или связный список типа LinkedList<String>. public class Overloader { private static String display(ArrayList<Integer> arrayList) { return "ArrayList"; } private static String display(LinkedList<String> linkedList) { return "LinkedList"; } private static String display(List<?> list) { return "List is not recognized"; } public static void main(String[] args) { // единственный объект типа ArrayList System.out.printin(display(new ArrayList<Integer>())); // списочный массив List<?>[] invokeAll = new List<?>[] { new ArrayList<Integer>(), new LinkedList<String>(), new Vector<Integer>()}; for (List<?> list : invokeAll) { System.out.printin(display(list)); } } }
230 Глава 5 Ложные представления программистов Во время компиляции массив объектов относится к типу List. Класс java, util .Vector не относится ни к типу ArrayList, ни к LinkedList, и поэтому предполагается, что должен быть выведен следующий результат: ArrayList, ArrayList LinkedList, и далее: List is not recognized (Список не распознан) А фактически три раза подряд выводится следующий результат: ArrayList и далее: List is not recognized Такое поведение кода объясняется тем, что на вызываемые перегружаемые методы оказы- вают воздействие только типы их аргументов, определяемые на стадии компиляции, т.е. тип ArrayList при первом вызове и тип List при последующих вызовах. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам корректно- го программирования на Java, применяется единственный метод display () и оператор instanceof для различения разных типов данных. Как и предполагалось, выводится следу- ющий результат: ArrayList, ArrayList LinkedList, List is not recognized public class Overloader { private static String display(List<?> list) { return ( list instanceof ArrayList ? ’'Arraylist" : (list instanceof LinkedList ? "LinkedList" : "List is not recognized") ); } public static void main(String[] args) { // единственный объект типа ArrayList System.out.printin(display(new ArrayList<Integer>())); List<?>[] invokeAll = new List<?>[] { new ArrayList<Integer>(), new LinkedList<String>(), new Vector<Integer>()}; for (List<?> list : invokeAll) { System.out.printin(display(list)); } } }
73. Не путайте неизменяемость ссылки и доступного по ссылке объекта 231 Применимость Неоднозначное применение перегрузки методов может привести к неожиданным резуль- татам. Библиография [API 2013] [Bloch 2008] [Tutorials 2013] Interface Collection<E> Item 41/'Use Overloading Judiciously" Defining Methods 73. He путайте неизменяемость ссылки и доступного по ссылке объекта Неизменяемость служит основанием для поддержки безопасности. Неизменяемые объ- екты безопасно делать общими, не подвергая их риску видоизменения со стороны получате- ля [Mettler 2010]. Программисты зачастую неверно предполагают, что объявление поля или переменной как final делает доступный по ссылке объект неизменяемым. Ведь объявление переменных примитивных типов как final не препятствует изменению их значений после инициализации в ходе обычной для Java обработки. Но когда переменная относится к ссылоч- ному типу, наличие в ее объявлении ключевого слова final делает неизменяемой только саму ссылку. Ключевое слово final не оказывает никакого влияния на доступный по ссылке объект. Следовательно, поля доступного по ссылке объекта могут быть изменяемыми. По этому пово- ду в §4.12.4 “Конечные переменные” спецификации JLS [JLS 2013] говорится следующее. “Если конечная переменная, объявленная как final, содержит ссылку на объект, то состояние объекта может быть изменено в результате выполняемых над ним операций. Но переменная будет всегда ссылаться на тот же самый объект”. Это же относится и к массивам, поскольку массивы являются объектами. Если конечная переменная содержит ссылку на массив, то его элементы могут быть по-прежнему изменены в результате операций над ним, но сама переменная всегда будет ссылаться на тот же самый массив. Аналогично параметр конечного (final) метода получает неизменяемую копию ссыл- ки на объект. И это не оказывает никакого влияния на изменяемость доступных по ссылке данных. Пример кода, не соответствующего принятым нормам (изменяемый класс и конечная ссылка) В данном примере кода, не соответствующего принятым нормам корректного програм- мирования на Java, ссылка на экземпляр объекта point объявлена как final при неверном допущении, что такая мера предотвращает видоизменение значений в переменных экземпляра х и у. Значения переменных экземпляра могут быть изменены после их инициализации, пос- кольку ключевое слово final применяется только к ссылке на экземпляр объекта point, но не к доступному по ссылке объекту. class Point { private int x; private int y;
232 Глава 5 Ложные представления программистов Point(int х, int у) { this.x = х; this.у = у; } void set_xy(int х, int у) { this.x = х; this.у = у; } void print_xy() { System.out.printin("the value x is: " + this.x); System.out.printin("the value у is: " + this.y); public class PointCaller { public static void main(String[] args) { final Point point = new Point(1, 2); point.print_xy(); // изменить значения координат x, у точки point.set_xy(5, 6); point.print_xy(); Решение, соответствующее принятым нормам (конечные поля) Если значения переменных экземпляра х и у должны оставаться неизменяемыми после их инициализации, то их следует объявить как final. Но тогда метод set_xy () становится недействительным, поскольку он уже не в состоянии изменять значения переменных экземп- ляра х и у. При таком видоизменении значения переменных экземпляра становятся неизме- няемыми, а следовательно, они соответствуют предполагаемой модели их применения. class Point { private final int x; private final int y; Point(int x, int y) { this.x = x; this.y = y; void print_xy() { System.out.printin("the value x is: " + this.x); System.out.printin("the value у is: " + this.y); // вызов метода set_xy(int x, int у) уже не возможен
73. Не путайте неизменяемость ссылки и доступного по ссылке объекта 233 Решение, соответствующее принятым нормам (предоставление функциональных возможностей для копирования) Если класс должен оставаться изменяемым, то в еще одном решении, соответствующем принятым нормам корректного программирования на Java, предоставляются функциональ- ные возможности для копирования. В данном решении предоставляется метод clone () из класса Point, и благодаря этому исключается потребность в методе установки. final public class Point implements Cloneable { private int x; private int y; Point(int x, int y) { this.x = x; this.у = у; } void set_xy(int x, int y) { this.x = x; this.у = у; } void print_xy() { System.out.printin("the value x is: "+ this.x); System.out.printin("the value у is: "+ this.y); } public Point clone () throws CloneNotSupportedException { Point cloned = (Point) super.cloneO; // клонировать переменные экземпляра x и у не требуется, // поскольку они относятся к примитивному типу return cloned; } } public class PointCaller { public static void main(String[] args) throws CloneNotSupportedException { Point point = new Point(1, 2); // не изменяется в методе main() point.print_xy(); // получить копию исходного объекта Point pointcopy = point.clone(); // переменная экземпляра pointCopy теперь содержит однозначную ссылку // вновь клонированный экземпляр объекта типа Point // изменить значение в копии координат х, у точки pointCopy.set_xy(5, 6); // исходное значение остается неизменным point.print_xy(); } } Метод clone () возвращает копию исходного объекта, отражающую состояние исходного объекта в момент клонирования. Этот новый объект может быть использован без раскры- тия исходного объекта. А поскольку в вызывающем коде содержится только ссылка на вновь
234 Глава 5 Ложные представления программистов клонированный экземпляр, то переменные экземпляра нельзя изменить без взаимодействия с вызывающим кодом. Такое применение метода clone () позволяет классу оставаться безо- пасно изменяемым, (см. также рекомендацию “OBJ04-J. Предоставляйте изменяемые классы с функциями копирования для надежной передачи экземпляров ненадежному коду” из стан- дарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]). Класс Point объявляется конечным (final), чтобы предотвратить переопределение ме- тода clone () в подклассах. Это позволяет использовать класс надлежащим образом без лю- бых случайных и неумышленных видоизменений исходного объекта. Пример кода, не соответствующего принятым нормам (массивы) В данном примере кода, не соответствующего принятым нормам корректного програм- мирования на Java, применяется открытый статический конечный (public static final) массив items. Клиенты могут обыкновенно видоизменять содержимое массива, несмотря на то, что объявление ссылки на этот массив как final препятствует видоизменению самой ссылки: public static final String[] items = {/* . . . */}; Решение, соответствующее принятым нормам (получение индекса) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, массив объявляется закрытым, а для получения отдельных элементов и размера массива предоставляются открытые методы. Предоставление прямого доступа к са- мим объектам массива вполне безопасно, поскольку класс String является неизменяемым. private static final String[] items = {/* . . . */}; public static final String getltem(int index) { return items[index]; } public static final int getltemCount() { return items.length; } Решение, соответствующее принятым нормам (клонирование массива) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, объявляется закрытый массив, а также открытый метод, возвращаю- щий копию массива. private static final String[] items = {/* . . . */}; public static final String [] getltemsO { return items.clone(); } В связи с тем что возвращается копия массива, исходные значения в массиве не могут быть изменены клиентом. Следует, однако, иметь в виду, что для обращения с массивами объектов может потребоваться полная, получаемая вручную копия массива. Как правило,
74. Аккуратно пользуйтесь методами сериализации... 235 это происходит в том случае, когда объекты не экспортируют метод clone (). Подробнее об этом см. в рекомендации “OBJ06-J. Защитное копирование изменяемых входных параметров и внутренних компонентов” из стандарта The CERT Oracle* Secure Coding Standard for Java™ [Long 2012]. Как и прежде, данный метод предоставляет прямой доступ к самим объектам массива, но такой доступ вполне безопасен, поскольку класс String является неизменяемым. Если бы массив содержал изменяемые объекты, метод get Items () мог бы вместо этого возвратить массив клонированных объектов. Решение, соответствующее принятым нормам (немодифицируемые оболочки) В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, объявляется закрытый массив, из которого составляется открытый неизменяемый список. private static final String[] items = {/* . . . */}; public static final List<String> itemsList = Collections.unmodifiableList(Arrays.asList(items)); Ни исходные значения в массиве, ни открытый список не могут быть видоизменены кли- ентом. Подробнее о немодифицируемых оболочках см. в рекомендации 3 “Снабжайте уязви- мые изменяемые классы немодифицируемыми оболочками”. Данное решение подходит и в том случае, когда массив содержит изменяемые объекты. Применимость Если неверно допустить, что благодаря конечным ссылкам (final) содержимое доступ- ного по ссылке объекта остается изменяемым, то совершающий атаку злоумышленник может видоизменить объект, который считается неизменяемым. Библиография [Bloch 2008] lt< [Core Java 2003] Cl [JLS 2013] §- [Long 2012] [Mettler 2010] Item 13, "Minimize the Accessibility of Classes and Members" Chapter 6, "Interfaces and Inner Classes" §4.12.4, "final Variables" §6.6, "Access Control" OBJ04-J. Provide mutable classes with copy functionality to safely allow passing instances to untrusted code OBJ06-J. Defensively copy mutable inputs and mutable internal components "Class Properties for Security Review in an Object-Capability Subset of Java" 74. Аккуратно пользуйтесь методами сериализации writeUnshared() и readUnshared() При сериализации объектов с помощью метода writeObject () каждый объект за- писывается в поток вывода только один раз. При вызове метода writeObject () для од- ного и того же объекта второй раз обратная ссылка на сериализованный ранее экземпляр
236 Глава 5 Ложные представления программистов направляется в поток вывода. Соответственно в методе readobject () получается хотя бы один экземпляр каждого объекта, присутствующего в потоке ввода и записанного ранее ме- тодом writeObject(). В документации на прикладной интерфейс Java API [API 2013] относительно метода writeUnshared () говорится следующее. “Этот метод записывает “неразделяемый” объект в поток вывода типа ObjectOutputStream. Он действует подобно методу writeObject (), за исключени- ем того, что всегда записывает заданный объект в поток вывода как новый, однознач- ный объект, в отличие от обратной ссылки на сериализованный ранее экземпляр”. Аналогично относительно метода readUnshared() в той же документации говорится следующее. “Этот метод читает “неразделяемый” объект из потока ввода типа Object Input St ream. Он действует подобно методу readOb j ect (), за исключением того, что предотвраща- ет возврат дополнительных ссылок при последующих вызовах методов readobject () и readUnshared () для десериализации экземпляра, получаемого в результате подоб- ного вызова”. Следовательно, методы writeUnshared () и readUnshared () непригодны для круговой сериализации структур данных, содержащих ссылочные циклы. Рассмотрим следующий при- мер кода. public class Person { private String name; PersonO { // не делать ничего из того, что требуется для сериализации } Person(String theName) { name = theName; } // другие подробности к данному примеру не относятся } public class Student extends Person implements Serializable { private Professor tutor; Student() { // не делать ничего из того, что требуется для сериализации } Student(String theName, Professor theTutor) { super(theName); tutor = theTutor; } public Professor getTutorO { return tutor; }
74 Аккуратно пользуйтесь методами сериализации... 237 ) public class Professor extends Person implements Serializable { private List<Student> tutees = new ArrayList<Student>(); Professor() { //не делать ничего из того, что требуется для сериализации } Professor(String theName) { super(theName); } public List<Student> getTutees () { return tutees; } /** * Метод checkTutees() проверяет, является ли данный * профессор наставником всех студентов */ public boolean checkTutees () { boolean result = true; for (Student stu: tutees) { if (stu.getTutor() != this) { result = false; break; } } return result; } } // ... Professor jane = new Professor("Jane"); Student able = new Student("Able", jane); Student baker = new Student("Baker", jane); Student Charlie = new Student("Charlie", jane); jane.getTutees().add(able); jane.getTutees().add(baker); jane.getTutees().add(charlie); System.out.printin("checkTutees returns: " + jane.checkTutees()); // выводит строку "checkTutees returns: true" Professor и Student относятся к типам, расширяющим базовый тип Person. У студен- та (т.е. объекта типа Student) имеется наставник типа Professor, а у профессора (т.е. объ- екта типа Professor) — список (типа ArrayList) студентов (т.е. объектов типа Student). В методе checkTutees () проверяется, является ли данный профессор наставником всех сту- дентов. И если он таковым является, то возвращается логическое значение true, в противном случае — логическое значение false. Допустим, профессор (объект Jane) является наставником трех студентов (объекты Able, Baker и Charlie). При использовании методов writeUnshared () и readUnshared () в классах этих объектов возникают затруднения, как демонстрируется в приведенном ниже примере кода, не соответствующего принятым нормам.
238 Глава 5 Ложные представления программистов Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам корректного програм- мирования на Java, предпринимается попытка сериализации данных с помощью метода writeUnshared(). String filename = "serial"; try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename))) { // сериализация с помощью метода writeUnshared() oos.writeUnshared(jane); } catch (Throwable e) { // обработать ошибку } // десериализация с помощью метода readUnshared() try (ObjectInputstream ois = new ObjectInputstream(new Fileinputstream(filename))){ Professor jane2 = (Professor)ois.readUnshared(); System.out.printin("checkTutees returns: " + jane2.checkTutees()); } catch (Throwable e) { // обработать ошибку } Но когда данные десериализуются с помощью метода readUnshared(), метод checkTutees () больше не возвращает логическое значение true, поскольку объекты на- ставников трех студентов отличаются от исходного объекта типа Professor. Решение, соответствующее принятым нормам В представленном здесь решении, соответствующем принятым нормам корректного про- граммирования на Java, методы writeObject () и readobject () используются для гарантии того, что объект наставника, на который ссылаются объекты трех студентов, имеет взаимно однозначное соответствие с исходным объектом типа Professor. Метод checkTutees () правильно возвращает логическое значение true. String filename = "serial"; try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename))) { // сериализация с помощью метода writeUnshared() oos.writeObj ect(j ane); } catch (Throwable e) { // обработать ошибку } // десериализация с помощью метода readUnshared() try (Objectinputstream ois = new ObjectInputstream(new Fileinputstream(filename))) { Professor jane2 = (Professor)ois.readobject(); System.out.printin("checkTutees returns: " + jane2.checkTutees()); } catch (Throwable e) { // обработать ошибку
75. Не пытайтесь оказывать помощь системе "сборки мусора"... 239 Применимость Вызов методов writeUnshared () и readUnshared () может дать неожиданные резуль- таты, если они используются для круговой сериализации структур данных, содержащих ссы- лочные циклы. Библиография [API 2013] Class ObjectOutputStream Class ObjectlnputStream 75. He пытайтесь оказывать помощь системе "сборки мусора", устанавливая пустое значение в локальных переменных ссылочного типа Устанавливать пустое значение в локальных переменных ссылочного типа совсем не обяза- тельно, чтобы помочь “сборке мусора”. Но такая мера лишь вносит беспорядок в код и способ- на затруднить его сопровождение. Динамический компилятор Java (JIT) способен выполнять равнозначный анализ живучести кода, что и осуществлено в большинстве его реализаций. С этим связана неудачная практика использовать метод завершения кода для обнуления ссылок. Подробнее об этом см. рекомендацию “MET12-J. Не пользуйтесь методами заверше- ния кода” из стандарта The CERT* Oracle* Secure Coding Standard for Java™ [Long 2012]. В час- тности, данная рекомендация распространяется на локальные переменные. А в том случае, когда оказывается полезным явное стирание объектов, обращайтесь к рекомендации 49 “Уда- ляйте объекты с коротким сроком действия из контейнерных объектов с длительным сроком действия”. Пример кода, не соответствующего принятым нормам В данном примере кода, не соответствующего принятым нормам корректного программи- рования на Java, в качестве локальной переменной используется буфер, в котором хранится ссылка на временный массив. А программист пытается помочь “сборке мусора”, присваивая пустое значение буферному массиву, когда он больше не требуется. { // локальная область действия int[] buffer = new int[100]; doSomething(buffer); buffer = null; } Решение, соответствующее принятым нормам Для логики программы периодически требуется строгий контроль над сроком действия объекта, доступного по ссылке из локальной переменной. В необычных случаях, когда та- кой контроль все же требуется, для ограничения области действия переменной используется лексический блок, поскольку “сборка мусора” позволяет освободить объект из памяти, как только он окажется вне области действия данной переменной [Bloch 2008]. В представленном здесь решении, соответствующем принятым нормам корректного программирования на Java, такой лексический блок служит для контроля над сроком действия объекта буфера.
240 Глава 5 Ложные представления программистов { // ограничить область действия буфера int[] buffer = new int[100]; doSomething(buffer); Применимость Устанавливать пустое значение в локальных переменных ссылочного типа совсем не обя- зательно, если они больше не нужны. Подобная попытка помочь системе “сборки мусора” освободить соответствующую область памяти является ошибкой. Библиография [Bloch 2008] Item 6, "Eliminate Obsolete Object References" [Long 2012] MET12-J. Do not use finalizers
Приложение Android В этом приложении описывается применимость рассмотренных в этой книге рекоменда- ций для целей разработки приложений на Java, предназначенных для платформы Android. Рекомендация Применимость 1. Ограничивайте срок действия уязвимых данных Применимо1 2. Не храните уязвимые данные незашифрованными на стороне клиента Применимо 3. Снабжайте уязвимые изменяемые классы немодифицируемыми оболочками Неизвестно 4. Вызывайте уязвимые для безопасности методы с проверенными аргументами Применимо в принципе2 5. Не допускайте выгрузку произвольных файлов Применимо в принципе 6. Кодируйте или экранируйте выводимые данные надлежащим образом Применимо в принципе 7. Предотвращайте внедрение кода Применимо в принципе3 8. Предотвращайте внедрение операторов Xpath Применимо 9. Предотвращайте внедрение операторов LDAP Применимо в принципе4 1. Приведенным примером кода, не соответствующего принятым нормам корректного программирова- ния на Java, труднее воспользоваться на виртуальной машине Dalvik VM, поскольку каждое приложе- ние выполняется на отдельной виртуальной машине Dalvik VM, что затрудняет доступ к строковым объектам из других приложений. 2. На платформе Android метод ccessControlContext () недоступен. 3. Класс ScriptEngineManager не входит в состав набора инструментов разработки Android SDK. 4. Применимо в принципе для тех приложений на платформе Android, где предпринимается попытка реализовать собственный протокол LDAP.
242 Приложение Android Рекомендация Применимость 10. Не пользуйтесь методом clone О для копирования небезопасных пара- метров метода 11. Не пользуйтесь методом Object.equals () для сравнения ключей шиф- рования 12. Не пользуйтесь небезопасными или слабыми алгоритмами шифрования 13. Храните пароли с помощью хеш-функции 14. Обеспечьте подходящее начальное случайное значение для класса SecureRandom 15. Не полагайтесь на методы, которые могут быть переопределены в ненадеж- ном коде 16. Старайтесь не предоставлять излишние полномочия 17. Сводите к минимуму объем привилегированного кода 18. Не раскрывайте методы с нестрогими проверками ненадежного кода 19. Определяйте специальные полномочия доступа для мелкоструктурной за- щиты 20. Создавайте безопасную "песочницу", используя диспетчер защиты 21. Не допускайте злоупотреблений привилегиями методов обратного вызова в ненадежном коде 22. Минимизируйте область действия переменных 23. Минимизируйте область действия аннотации @SuppressWarnings 24. Минимизируйте доступность классов и их членов 25. Документируйте потоковую безопасность и пользуйтесь аннотациями везде, где только можно 26. Всегда предоставляйте отклик на результирующее значение метода 27. Распознавайте файлы, используя несколько файловых атрибутов 28. Не присоединяйте значимость к порядковому значению, связанному с пере- числением 29. Принимайте во внимание числовое продвижение типов 30. Активизируйте проверку типов в методах с переменным количеством аргу- ментов во время компиляции 31. Не объявляйте открытыми и конечными константы, значения которых могут измениться в последующих выпусках программы Применимо Применимо Применимо Применимо Применимо Применимо Не применимо5 Не применимо5 Не применимо Не применимо Не применимо Неизвестно Применимо Применимо Применимо Применимо Применимо Применимо в принципе6 Применимо Применимо Применимо Применимо 5. Класс Accesscontroller на платформе Android не применяется. 6. Методы openFileOutput () и openFilelnput() предпочтительнее применять для файлового ввода-вывода на платформе Android. Рекомендация Применимость 32. Избегайте циклических зависимостей пакетов Применимо 33. Отдавайте предпочтение определяемым пользователем исключениям над более общими типами исключений Применимо 34. Старайтесь изящно исправлять системные ошибки Применимо 35. Тщательно разрабатывайте интерфейсы, прежде чем их выпускать Применимо 36. Пишите код, удобный для "сборки мусора" Применимо
Приложение Android 243 Окончание таблицы Рекомендация Применимость 37. Не затеняйте и не заслоняйте идентификаторы в подобластях действия 38. Не указывайте в одном объявлении больше одной переменной 39. Пользуйтесь описательными символическими константами для обозначения литеральных значений в логике программы 40. Правильно кодируйте отношения в определениях констант 41. Возвращайте из методов пустой массив или коллекцию вместо пустого значения 42. Пользуйтесь исключениями только в особых случаях 43. Пользуйтесь оператором try с ресурсами для безопасного обращения с закрываемыми ресурсами 44. Не пользуйтесь утверждениями для проверки отсутствия ошибок при выполнении 45. Пользуйтесь вторым и третьим однотипными операндами в условных выражениях 46. Не выполняйте сериализацию прямых описателей системных ресурсов 47. Отдавайте предпочтение итераторам над перечислениями 48. Не пользуйтесь прямыми буферами для хранения нечасто используемых объектов с коротким сроком действия 49. Удаляйте объекты с коротким сроком действия из контейнерных объектов с длительным сроком действия 50. Будьте внимательны, применяя визуально дезориентирующие идентификаторы и литералы 51. Избегайте неоднозначной перегрузки методов с переменным количеством аргументов 52. Избегайте внутренних индикаторов ошибок 53. Не выполняйте операции присваивания в условных выражениях 54. Пользуйтесь фигурными скобками в теле условного оператора if, а также циклов for или while 55. Не ставьте точку с запятой сразу после условного выражения с оператором if, for или while Применимо Применимо Применимо Применимо Применимо Применимо Не применимо7 Применимо в принципе8 Применимо Применимо Применимо Применимо Применимо Применимо Применимо Применимо Применимо Применимо Применимо 7. Версия Java 7 на платформе Android SDK в настоящее время не поддерживается, и поэтому оператор try с ресурсами на этой платформе недоступен. 8. Метод assert () на платформе Android SDK игнорируется по умолчанию. Рекомендация Применимость 56. Завершайте каждый набор операторов, связанных с меткой case, опера- тором break 57. Избегайте неумышленного зацикливания счетчиков циклов 58. Пользуйтесь круглыми скобками для обозначения операций предшество- вания 59. Не делайте никаких предположений о создании файлов 60. Преобразуйте целые значения в значения с плавающей точкой для выпол- нения операций с плавающей точкой Применимо Применимо Применимо Применимо в принципе9 Применимо
244 Приложение Android Окончание таблицы Рекомендация Применимость 61. Вызывайте метод super. clone () из метода clone () Применимо 62. Употребляйте комментарии единообразно и в удобном для чтения виде Применимо 63. Выявляйте и удаляйте излишний код и значения Применимо 64. Стремитесь к логической полноте Применимо 65. Избегайте неоднозначной или вносящей путаницу перегрузки Применимо 66. Не принимайте на веру, что объявление изменчивой ссылки гарантирует Применимо надежную публикацию членов объекта, доступного по этой ссылке 67. Не принимайте на веру, что методы sleep (), yield () или Применимо getState () предоставляют семантику синхронизации 68. Не принимайте на веру, что оператор вычисления остатка всегда возвра- Применимо щает неотрицательный результат для целочисленных операндов 69. Не путайте равенство абстрактных объектов с равенством ссылок Применимо 70. Ясно различайте поразрядные и логические операторы Применимо 71. Правильно интерпретируйте управляющие символы при загрузке строк Применимо 72. Не пользуйтесь перегружаемыми методами для динамического различе- Применимо ния типов данных 73. Не путайте неизменяемость ссылки и доступного по ссылке объекта Применимо 74. Аккуратно пользуйтесь методами сериализации writeUnshared () и Применимо readUnshared() 75. Не пытайтесь оказывать помощь системе "сборки мусора", устанавливая Применимо пустое значение в локальных переменных ссылочного типа 9. Пакет java. nio. f ile на платформе Android недоступен.
Приложение Словарь специальных терминов Атомарность. Применительно к операции над данными примитивных типов обозначает, что данные могут быть доступны в других потоках перед началом операции или по ее завер- шении. Но промежуточные значения данных могут оказаться недоступными в других потоках. Брешь в защите. Изъян в программном обеспечении, подвергающий его риску возможно- го нарушения безопасности [Seacord 2013]. Гонка данных. “Когда в программе имеют место два конфликтующих доступа, которые не упорядочены заранее установленным отношением, то говорят, что возникает гонка данных” [JLS 2013, §17.4.5 “Заранее установленный порядок”]. Динамическая область памяти. “Область оперативной памяти, разделяемая потоками и называемая иначе “кучей”. Все поля экземпляра, статические поля и элементы массива хра- нятся в динамической области памяти... Локальные переменные, параметры формальных методов или обработчиков исключений вообще не разделяются потоками и не подвержены воздействию со стороны модели памяти” [JLS 2013, §17.4.1 “Общие переменные”]. Живучесть. Означает, что каждая операция или вызов метода выполняется до полного завершения без прерываний, даже если это противоречит правилам безопасности. Заранее установленный порядок. “Два действия можно упорядочить по заранее установ- ленному отношению. Так, если одно действие происходит раньше другого, то первое из них становится доступным и упорядочивается прежде другого... Следует, однако, иметь в виду, что наличие заранее установленного отношения между двумя действиями совсем не означает, что в конкретной реализации они должны происходить именно в таком порядке. Если пере- упорядочение действий приводит к результатам, согласующимся с допустимым исполнени- ем, то такое изменение порядка действий не считается запрещенным. Так, если между двумя действиями заранее установлено отношение, они совсем не обязательно должны происходить именно в таком порядке в любом коде, где между ними заранее не установлено отношение. Например, операции записи в одном потоке могут происходить не по порядку с операция- ми чтения в другом потоке, в результате чего возникает состояние гонки данных” [JLS 2013, §17.4.5 “Заранее установленный порядок”].
246 Приложение Б Словарь специальных терминов Заслонение. Один идентификатор заслоняет другой в охватывающей их области действия, если оба идентификатора одинаковы, но заслоняющий идентификатор не затеняет заслоняе- мый идентификатор. Такое может, в частности, произойти в том случае, если заслоняющий идентификатор обозначает переменную, а заслоняемый идентификатор — тип данных. Под- робнее об этом см. в §6.4.2 “Заслонение” спецификации JLS [JLS 2013]. Ср. с затенением. Затенение. Один идентификатор затеняет другой в охватывающей их области действия, если оба идентификатора одинаковы и обозначают переменные, а возможно, и методы или типы данных. Затененный идентификатор недоступен в области действия затеняющего иден- тификатора. Подробнее об этом см. в §6.4.1 “Затенение” спецификации JLS [JLS 2013]. Ср. с заслонением. Изменчивость. “Запись данных в изменчивое поле происходит перед любым последую- щим чтением данных из этого поля” [JLS 2013, §17.4.5, “Happens-before Order”]. “Операции над главными копиями изменчивых переменных от имени потока выполняются в основной памяти точно в таком же порядке, в каком запрашивается поток” [JVMSpec 1999]. Последова- тельный доступ к изменчивой переменной осуществляется согласованно, и это также означает, что выполняемая компилятором оптимизация на подобные операции не распространяется. Объявление переменной изменчивой (volatile) гарантирует, что во всех потоках становится доступным самое последнее значение этой переменной, если оно видоизменяется в любом по- токе. Изменчивость гарантирует атомарность операций записи и чтения значений примитив- ных типов данных, но не гарантирует атомарность таких составных операций, как приращение значения переменной (т.е. последовательность операций чтения, видоизменения и записи). Каноническое представление. Сведение вводимых данных к эквивалентной и самой простой из известных форм. Модель памяти. “Правила, определяющие порядок доступа к памяти и гарантирующие доступ к ней в определенные моменты времени, называются моделью памяти в языке про- граммирования Java” [Arnold 2006]. “Модель памяти описывает, является ли трассировка вы- полнения допустимым порядком исполнения отдельной программы” [JLS 2013, §17.4 “Модель памяти”]. Надежная публикация. “Для надежной публикации объекта ссылка на него и [состояние объекта] должны стать одновременно доступными из других потоков. Объект, построенный надлежащим образом, может быть надежно опубликован такими способами: инициализация ссылки на объект из статического инициализатора; сохранение ссылки на объект в изменчивом (volatile) поле или переменной ссылки на объект типа AtomicRef erence; сохранение ссылки на объект в конечном (final) поле объекта, построенного надле- жащим образом; сохранение ссылки на объект в поле, защищенном надлежащим образом блокировкой” [Goetz 2006, §3.5 “Safe Publication”]. Надежность. Мера, главная цель которой состоит в поддержании согласованности состо- яний всех объектов в многопоточной среде [Lea 2000]. Надежный код. Код, загружаемый изначальным загрузчиком классов независимо от того, входит ли он в состав прикладного интерфейса Java API. В данном контексте это понятие рас- ширено и включает в себя код, получаемый из известного источника и получающий полно- мочия, отсутствующие у ненадежного кода. В соответствии с таким определением надежный и ненадежный коды могут сосуществовать в пространстве имен одного (но не обязательно
Приложение Б Словарь специальных терминов 247 изначального) загрузчика классов. В подобных случаях правила безопасности должны прово- дить ясное различие между обеими разновидностями кода, назначая соответствующие полно- мочия для надежного кода и отказывая в них ненадежному коду. Неизменяемый. Применительно к объекту термин неизменяемый означает, что его состо- яние не может быть изменено после инициализации. Объект становится неизменяемым при следующих условиях: его состояние не может быть видоизменено после построения; все его поля являются конечными; он построен надлежащим образом (т.е. ссылка this не исчезает во время его постро- ения) [Goetz 2006]. Формально вполне возможно построить неизменяемый объект, не все поля которого яв- ляются конечными (final). Примером тому служит класс String, но такая возможность опирается на тонкое умозаключение о доброкачественных гонках данных, требующее глубо- кого понимания модели памяти в Java (Java Memory Model). Ненадежный код. Код неизвестного происхождения, который может нанести определен- ный вред при своем выполнении. Ненадежный код не всегда может оказаться злонамеренным, но, как правило, его трудно выявить автоматически. Следовательно, ненадежный код должен выполняться в среде, ограниченной “песочницей”. Нормализация. Преобразование данных с потерями в простейшую (и ожидаемую) из всех известных форм. “Если в конкретных реализациях символьные строки сохраняются в нормализованной форме, то можно гарантировать, что эквивалентные символьные строки будут иметь однозначное двоичное представление” [Davis 2008]. Переменная класса. “Переменная класса — это поле, объявляемое с помощью ключево- го слова static в объявлении класса или же интерфейса как с помощью ключевого слова static, так и без него. Переменная класса создается при подготовке ее класса или интерфей- са, а инициализируется значением по умолчанию. По существу, переменная класса прекраща- ет свое существование при выгрузке своего класса или интерфейса” [JLS 2013, §4.12.3 “Виды переменных”]. Переопределение. Один метод из класса переопределяет другой метод из суперкласса, если у них совместимые сигнатуры. Переопределяемый метод по-прежнему доступен из клас- са посредством ключевого слова super. Формальное определение понятия переопределение см. в §8.4.8.1 “Переопределение методов экземпляра” спецификации JLS [JLS 2013]. Ср. с со- крытием. Потокобезопасный. Объект считается потокобезопасным, если он может разделяться не- сколькими потоками, т.е. быть общим для них, исключая возможность возникновения гонок данных. “Потокобезопасный объект выполняет синхронизацию внутренним образом, чтобы обеспечить свободный доступ к нему из нескольких потоков через его открытый интерфейс без дополнительной синхронизации” [Goetz 2006]. Неизменяемые классы являются потоко- безопасными по определению. А изменяемые классы также могут быть потокобезопасными, если они синхронизированы надлежащим образом. Предикат условия. Выражение, составляемое из переменных состояния класса, которые должны принимать логическое значение true для продолжения исполнения потока. Испол- нение потока приостанавливается с помощью методов Object. wait () и Thread. sleep () или какого-нибудь другого механизма, а затем возобновляется, предположительно, когда тре- бование истинно и уведомляется об этом [Goetz 2006].
248 Приложение Б Словарь специальных терминов Публикация объектов. “Публикация объекта означает предоставление доступа к нему из кода, находящегося за пределами текущей области действия данного объекта. С этой целью ссылка на объект сохраняется там, где ее можно обнаружить в другом коде, возвращается из незакрытого метода или передается методу из другого класса” [Goetz 2006]. Санобработка. Проверка достоверности вводимых данных и последующее их преобразо- вание к форме представления, соответствующей требованиям, предъявляемым к вводу дан- ных в сложных системах. Например, в базе данных может потребоваться экранирование или исключение всех недостоверных символов перед сохранением вводимых данных. Санобра- ботка вводимых данных позволяет исключить из них все нежелательные и лишние символы путем удаления, замены, кодирования или экранирования. Синхронизация. “В языке программирования Java предоставляется несколько механиз- мов для организации взаимодействия потоков. К числу самых основных механизмов относит- ся синхронизация, реализуемая с помощью мониторов. С каждым объектом в Java связывается отдельный монитор, который может блокироваться или разблокироваться потоком. Блоки- ровку по монитору может одновременно удерживать только один поток. А любые другие по- токи, пытающиеся заблокировать данный монитор, блокируются до тех пор, пока не получат блокировку по данному монитору” [JLS 2013, §17.1, “Синхронизация”]. Сокрытие. Одно поле из класса скрывает другое поле из суперкласса, если у них одинако- вый идентификатор. Скрытое поле недоступно из класса. Аналогично один метод из класса скрывает другой метод из суперкласса, если у них одинаковый идентификатор, но несовмес- тимые сигнатуры. Скрытый метод недоступен из класса. Формальное определение понятия сокрытие см. в §8.4.8.2 “Сокрытие методов в классах” спецификации JLS [JLS 2013]. Ср. с пере- определением. Управляющее выражение. Это выражение верхнего уровня в условном операторе if, цикле while, do. . . while или операторе switch. Условие гонок. “Вообще, гонки приводят к недетерминированному исполнению и сбоях в программах, поведение которых предполагается детерминированным” [Netzer 1992]. “Условие гонок возникает в том случае, если правильность вычислений зависит от относительной син- хронизации или чередования нескольких исполняемых потоков в исполняющей среде” [Goetz 2006]. Уязвимость. “Ряд условий, позволяющих совершающему атаку злоумышленнику явно или неявно нарушить установленные правила безопасности” [Seacord 2013]. Уязвимые данные. Любые данные, которые должны надежно храниться. К последствиям такого требования к надежному хранению данных относятся следующие: запрещается доступ к уязвимым данным из ненадежного кода; в запрещается утечка уязвимых данных из надежного кода в ненадежный код. К примерам уязвимых данных относятся пароли и данные, идентифицирующие лич- ность. Уязвимый код. Любой код, выполняющий операции, которые должны быть запрещены в ненадежном коде. А также любой код, получающий доступ к уязвимым данным. Например, код, для правильного выполнения которого требуются расширенные полномочия, обычно считается уязвимым.
Приложение Библиография [Allen 2000] Vermeulen, Allan, Scott W. Ambler, Greg Bumgardner, Eldon Metz, Trevor Misfeldt, Jim Shur, and Patrick Thompson. The Elements of Java Style. New York, NY: Cambridge University Press (2000). [Apache 2013] Apache Tika: A Content Analysis Toolkit. The Apache Software Foundation (2013).http://tika.apache.org/index.html [API 2006] Java' Platform, Standard Edition 6 API Specification. Oracle (2006/2011). http: / / docs.oracle.com/javase/6/docs/api/ [API 2013] Java' Platform, Standard Edition 7 API Specification. Oracle (2013). http: / /docs. oracle.com/j avase/7/docs/api/index.html [Arnold 2006] Arnold, Ken, James Gosling, and David Holmes. The Java Programming Language, Fourth Edition. Boston, MA: Addison-Wesley (2006). [Bloch 2001] Bloch, Joshua. Effective Java : Programming Language Guide. Boston, MA: Addison- Wesley (2001). [Bloch 2005] Bloch, Joshua, and Neal Gafter. Java' Puzzlers: Traps, Pitfalls, and Corner Cases. Boston, MA: Addison-Wesley (2005). [Bloch 2008] Bloch, Joshua. Effective Java": Programming Language Guide, Second Edition. Boston, MA: Addison-Wesley (2008). [Campione 1996] Campione, Mary, and Kathy Walrath. The Java' Tutorial: Object-Oriented Programming for the Internet. Reading, MA: Addison-Wesley (1996). [Chan 1998] Chan, Patrick, Rosanna Lee, and Douglas Kramer. The Java” Class Libraries: Supplement for the Java™ 2 Platform, Volume 1, Second Edition. Upper Saddle River, NJ: Prentice Hall (1998). [Conventions 2009] Code Conventions for the Java Programming Language. Oracle (2009). www.oracle.com/technetwork/java/codeconv-138413.html [Coomes 2007] Coomes, John, Peter Kessler, and Tony Printezis. “Garbage Collection- Friendly Programming.” JavaOne Conference (2007). http://docs.huihoo.com/javaone/ 2007/java-se/TS-2906.pdf
250 Приложение В Библиография [Core Java 2003] Horstmann, Cay S., and Gary Cornell. Core Java 2, Volume I: Fundamentals, Seventh Edition. Upper Saddle River, NJ: Prentice Hall (2003). [Coverity 2007] Coverity Prevent” Users Manual (3.3.0). Coverity (2007). [Daconta 2003] Daconta, Michael C., Kevin T. Smith, Donald Avondolio, and W. Clay Richardson. More Java Pitfalls: 50 New Time-Saving Solutions and Workarounds. Indianapolis, IN: Wiley (2003). [Davis 2008] Unicode Standard Annex #15: Unicode Normalization Forms, ed. Mark Davis and Ken Whistler. Unicode (2008). http: //unicode.org/reports/trl5/ [ESA 2005] Java Coding Standards. ESA Board for Software Standardisation and Control (BSSC) (2005). http://software.ucv.ro/~eganea/SoftE/JavaCodingStandards.pdf [FindBugs 2008] FindBugs Bug Descriptions (2008/2011). http: //findbugs. sourceforge. net/bugDescriptions.html [Flanagan 2005] Flanagan, David. Java' in a Nutshell, Fifth Edition. Sebastopol, CA: O’Reilly (2005). [Fortify 2013] A Taxonomy of Coding Errors That Affect Security, “Java/JSP” Fortify Software (2013). www.hpenterprisesecurity.com/vulncat/en/vulncat/index.html [GNU 2013] GNU Coding Standards, §5.3, “Clean Use of C Constructs.” Richard Stallman and other GNU Project volunteers (2013). www.gnu.org/prep/standards/standards. htmltfSyntactic-Conventions [Goetz 2004] Goetz, Brian. Java Theory and Practice: Garbage Collection and Performance: Hints, Tips, and Myths about Writing Garbage Collection-Friendly Classes. IBM developerWorks (2004). www.ibm.com/developerworks/java/library/j-jtp01274/index.html [Goetz 2006] Goetz, Brian, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, and Doug Lea. Java Concurrency in Practice. Boston, MA: Addison-Wesley (2006). [Goetz 2007] Goetz, Brian. Java Theory and Practice: Managing Volatility: Guidelines for Using Volatile Variables. IBM developerWorks (2007). www.ibm.com/developerworks/java/ library/j-j tpO 6197/index.html [Gong 2003] Gong, Li, Gary Ellison, and Mary Dageforde. Inside Java' 2 Platform Security: Architecture, API Design, and Implementation, Second Edition. Boston, MA: Addison-Wesley (2003). [Goodliffe 2007] Goodliffe, Pete. Code Craft: The Practice of Writing Excellent Code. San Francisco, CA: No Starch Press, (2007). [Grand 2002] Grand, Mark. Patterns in Java', Volume 1: A Catalog of Reusable Design Patterns Illustrated with UML, Second Edition. Indianapolis, IN: Wiley (2002). [Grubb 2003] Grubb, Penny, and Armstrong A. Takang. Software Maintenance: Concepts and Practice, Second Edition. River Edge, NJ: World Scientific (2003). [Guillardoy 2012] Guillardoy, Esteban. Java ODay Analysis (CVE-2012-4681). (2012). http: / / immunityproducts.blogspot.com.ar/2012/08/java-Oday-analysiscve-2012-4681. html [Hatton 1995] Hatton, Les. Safer C: Developing Software for High-integrity and Safety-critical Systems. New York, NY: McGraw-Hill, (1995). [Hawtin 2006] Hawtin, Thomas, [drlvm] [kernel_classes] ThreadLocal Vulnerability. MarkMail (2006). http: //markmail.org/message/4scermxmn5oqhyi [Havelund 2009] Havelund, Klaus, and Al Niessner. JPL Coding Standard, Version 1.1. California Institute of Technology (2009). http://lars-lab.jpl.nasa.gov/JPL_Coding_Standard_ Java.pdf [Hirondelle 2013] Passwords Never Clear in Text. Hirondelle Systems (2013). www. j avapractices.com/topic/TopicAction.do?Id=216
Приложение В Библиография 251 [ISO/IEC 9126-1:2001] Software Engineering—Product Quality, Part 1, Quality Model (ISO/IEC 9126-1:2001). Geneva, Switzerland: International Organization for Standardization (2001). [ISO/IEC/IEEE 24765:2010] Software Engineering—Product Quality Part 1, Quality Model (ISO/ IEC/IEEE 24765:2010). Geneva, Switzerland: International Organization for Standardization (2010). [JLS 2013] Gosling, James, Bill Joy, Guy Steele, Gilad Bracha, and Alex Buckley. The Java Language Specification: Java SE 7 Edition. Oracle America, Inc. (2013). http: / /docs .oracle, com/javase/specs/jIs/se7/html/index.html [JVMSpec 1999] The Java™ Virtual Machine Specification, Second Edition. Sun Microsystems, Inc. (1999). http://docs.oracle.com/javase/specs/ [Kalinovsky 2004] Kalinovsky, Alex. Covert Java : Techniques for Decompiling, Patching, and Reverse Engineering. Indianapolis, IN: SAMS (2004). [Knoernschild 2002] Knoernschild, Kirk. Java" Design: Objects, UML, and Process. Boston, MA: Addison-Wesley (2002). [Lea 2000] Lea, Doug. Concurrent Programming in Java": Design Principles and Patterns, Second Edition. Boston, MA: Addison-Wesley (2000). [Lo 2005] Lo, Chia-Tien Dan, Witawas Srisa-an, and J. Morris Chang. “Security Issues in Garbage Collection.” STSC Crosstalk (2005). www.eng.auburn.edu/users/hamilton/security/ papers/STSC%20CrossTalk%20-%20Security%20Issues%20in%20Garbage%20 Collection%20-%200ct%a02005.pdf [Long 2012] Long, Fred, Dhruv Mohindra, Robert C. Seacord, Dean E Sutherland, and David Svoboda. The CERT Oracle' Secure Coding Standard for Java. Boston, MA: Addison-Wesley (2012). [Manion 2013] Manion, Art. “Anatomy of Java Exploits,” CERT/CC Blog (2013). www.cert. org/blogs/certcc/2013/01/anatomy_of_java_exploits.html [McGraw 1999] McGraw, Gary, and Ed Felten. Securing Java: Getting Down to Business with Mobile Code, Second Edition. New York, NY: Wiley (1999). [Mettler 2010] Mettler, Adrian, and David Wagner. “Class Properties for Security Review in an Object-Capability Subset of Java.” Proceedings of the 5th ACM SIGPLAN Workshop on Programming Languages and Analysis for Security (PLAS TO). New York, NY: ACM (2010). DOI: 10.1145/1814217.1814224. http://dl.acm.org/citation.cfm?doid=1814217.1814224 [Miller 2009] Miller, Alex. Java" Platform Concurrency Gotchas. JavaOne Conference (2009). [Netzer 1992] Netzer, Robert H. B., and Barton P. Miller. “What Are Race Conditions? Some Issues and Formalization.” ACM Letters on Programming Languages and Systems l(l):74-88 (1992). http://dl.acm.org/citation.cfm?id=130616.130623 [Oaks 2001] Oaks, Scott. Java" Security. Sebastopol, CA: O’Reilly (2001). [Oracle 2010a] Java SE 6 HotSpot’ Virtual Machine Garbage Collection Tuning. Oracle (2010). www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html [Oracle 2010b] New I/O APIs. Oracle (2010). http: / /docs.oracle.com/javase/1.5.0/ docs/guide/nio/ [Oracle 2011a] Java’ PKI Programmers Guide. Oracle (2011). http://docs.oracle.com/ javase/6/docs/technotes/guides/security/certpath/CertPathProgGuide.html [Oracle 2011b] Java SE 6 Documentation. Oracle (2011). http://docs.oracle.com/ j avase/6/docs/index.html [Oracle 2011c] Package javax. servlet .http. Oracle (2011). http://docs.oracle.com/ javaee/6/api/javax/servlet/http/package-summary.html [Oracle 201 Id] Permissions in the Java’ SE 6 Development Kit (JDK). Oracle (2011). http: / / docs.oracle.com/javase/6/docs/technotes/guides/security/permissions.html
252 Приложение В Библиография [Oracle 2013а] API for Privileged Blocks. Oracle (2013). http://download.java.net/ jdk8/docs/technotes/guides/security/doprivileged.html [Oracle 2013b] “Reading ASCII Passwords from an Inputstream Example,” Java" Cryptography Architecture (JCA) Reference Guide. Oracle (2013). http://docs.oracle.eom/javase/7/ docs/technotes/guides/security/crypto/CryptoSpec.html#ReadPassword [Oracle 2013c] Java Platform Standard Edition 7 Documentation. Oracle (2013). http: / /docs. oracle.com/javase/7/docs/ [Oracle 2013d] Oracle Security Alert for CVE-2013-0422. Oracle (2013). www.oracle.com/ technetwork/topics/security/alert-cve-2013-0422-1896849.html [OWASP 2009] Session Fixation in Java. OWASP (2009). https: //www.owasp.org/index. php/Session_Fixation_in_Java [OWASP 2011] Cross-site Scripting (XSS). OWASP (2011). www.owasp.org/index.php/ Cross-site_Scripting_%28XSS%29 [OWASP 2012] “Why Add Salt?” Hashing Java. OWASP (2012). www.owasp.org/index. php/Ha shing_Java [OWASP 2013] OWASP Guide Project. The Open Web Application Security Project (OWASP) (2013). www. owasp.org/index.php/OWASP_Guide_Project [Paar 2010] Paar, Christof, and Jan Pelzl. Understanding Cryptography: A Textbook for Students and Practitioners. Heidelberg, NY: Springer (2010). [Pistoia 2004] Pistoia, Marco, Nataraj Nagaratnam, Larry Koved, and Anthony Nadalin. Enterprise Java' Security: Building Secure J2EE~ Applications. Boston, MA: Addison-Wesley (2004). [Policy 2010] Default Policy Implementation and Policy File Syntax, Document revision 1.6. Oracle (2010). http: / /docs . oracle . com/j avase/1.4.2/docs/guide/security/ PolicyFiles.html [SCG 2010] Secure Coding Guidelines for the Java Programming Language, Version 4.0. Oracle (2010). www.oracle.com/technetwork/java/seccodeguide-139067.html [Seacord 2009] Seacord, Robert C. The CERT C Secure Coding Standard. Boston, MA: Addison- Wesley (2009). [Seacord 2012] Seacord, Robert C., Will Dormann, James McCurley, Philip Miller, Robert Stoddard, David Svoboda, and Jefferson Welch. Source Code Analysis Laboratory (SCALe) (CMU/SEI- 2012-TN-013). Pittsburgh, PA: Carnegie Mellon University (2012). www. sei.cmu.edu/library/ abstracts/reports/12tn013.cfm [Seacord 2013] Seacord, Robert C. Secure Coding in C and C++, Second Edition. Boston, MA: Addison-Wesley (2013). См. также www.cert.org/books/secure-codingfor news and errata. [SecuritySpec 2010] Java Security Architecture. Oracle (2010). http: //docs.oracle. com/ javase/1.5.O/docs/guide/security/spec/security-specTOC.fm.html [Sen 2007] Sen, Robi. Avoid the Dangers ofXPath Injection. IBM developerWorks (2007). www. ibm.com/developerworks/xml/library/x-xpathinjection/index.html [Sethi 2009] Sethi, Amit. Proper Use of Java’s SecureRandom. Cigital Justice League Blog (2009). www.cigital.com/justice-league-blog/2009/08/14/properuse-of-javas- securerandom/ [Steinberg 2008] Steinberg, Daniel H. Using the Varargs Language Feature. Java Developer Connection Tech Tips (2008). www.java-tips.org/java-se-tips/java.lang/using-the- varargs-language-feature.html [Sterbenz 2006] Sterbenz, Andreas, and Charlie Lai. Secure Coding Antipatterns: Avoiding Vulnerabilities. JavaOne Conference (2006). https://confluence.ucdavis.edu/ confluence/download/attachments/16218/ts-1238.pdf?version=l&modification date=1180213302000
Приложение В Библиография 253 [Sutherland 2010] Sutherland, Dean Е, and William L. Scherlis. “Composable Thread Coloring” Из книги Proceedings of the 15th ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming (PPoPP ‘10). New York, NY: ACM (2010). http://dl.acm.org/citation. cfm?doid=1693453.1693485 [Tutorials 2013] The Java" Tutorials. Oracle (2013). http://docs.oracle.com/javase/ tutorial/index.html [Unicode 2013] Unicode 6.2.0. Mountain View, CA: The Unicode Consortium (2013). www. unicode.org/versions/Unicode6.2.0/ [Viega 2005] Viega, John. CLASP Reference Guide, Volume 1.1. Secure Software, 2005. [W3C 2003] The World Wide Web Security FAQ. World Wide Web Consortium (W3C) (2003). www.w3.org/Security/Faq/wwwsf2.html [Ware 2008] Ware, Michael S. Writing Secure Java Code: A Taxonomy of Heuristics and an Evaluation of Static Analysis Tools. James Madison University (2008). http://mikeware.us/ thesis/ [Zadegan 2009] Zadegan, Bryant. A Lesson on Infinite Loops, winjade.net (2009). http:// winjade.net/2009/01/lesson-on-infinite-loops/
Предметный указатель А Алгоритмы шифрования надежные, безопасное применение, 54 слабые, небезопасное применение, 53 Аннотации @GuardedBy, применение, 103 @Immutable, применение, 102 @NotThreadSafe, применение, 102 @Region и @RegionLock, применение, 102 @SuppressWarnings назначение, 94 ограничение области действия, 95 @ThreadSafe, применение, 101 документирование правил блокировки, 103 привязки к потоку, 105 потоковой безопасности, 101 протоколов ожидания и уведомления, 105 назначение, 100 параллелизма, разновидности и получение, 101 проверка средствами SureLogic, 101 Б Библиография, 249 Библиотеки и каркасы приложений, 14 Буферы непрямые, создание и удаление, 166 прямые особенности применения, 166 создание и удаление , 166 В Внедрение кода меры защиты, 40-41 условия, 40 операторов LDAP меры защиты, 48 условия и примеры, 46 операторов XPath меры защиты, 44; 46 условия и примеры, 42 Внутренние индикаторы ошибок назначение, 177 нежелательное применение, 177 типичные примеры, 177 Гонки исключение появления окна, 114 типа TOCTOU, условия возникновения, 106; 112; 114 д Диспетчеры защиты назначение, 81 поддерживаемые возможности, 81 применение, 81 проверки безопасности, 82 установка из командной строки, 81-82 программная, 81 по умолчанию, 84 3 Загрузчики классов, применение, 71 Заранее установленные отношения гарантия, 211 назначение, 210 синхронизация, 211 Заслонение, определение, 140 Затенение исключение, 141 определение, 140 И Идентификаторы визуально различаемые, применение, 171 обозначение, способы, 172 Интерфейсы Enumeration, применение, 166 Iterator, применение, 165 назначение, 132 особенности публикации, 135 последствия некачественной разработки, 132 реализация, 132 Исключения назначение, 130 непроверяемые, перехват, 130 обработка в коде, 129 передача управления обработчику, 132 общих типов, генерирование, 128 перехват по типу, 128 применение вместо кодов ошибок, 177 Итераторы предпочтение над перечислениями, 164 применение, 165 К Классы AtomiclntegerArray, назначение, 211 Random, безопасное применение, 59 абстрактные, 133 вложенные, порядок объявления, 97 клонирование подклассов, 195 конечные, применение, 63; 64 неизменяемые, документирование потоковой безопасности, 102 непроверяемых исключений, 130 перегрузка конструкторов, особенности, 205 порядок объявления членов, 97 принадлежность защищенным областям, 66 управление доступом, правила, 96 уязвимые, меры защиты, 30 Кодирование и экранирование выводимых данных, 39 Код излишний, выявление и исключение, 198
Предметный указатель 255 параллельно выполняемый, надежность, 215 пассивный выявление и исключение, 199 определение, 198 Комментарии, правильное употребление, 197 Константы определение в выражениях, 148 особенности объявления, 125 ошибки при объявлении, 123 порядок объявления, 145 предопределенные, применение, 147 символические, применение, 145 Круглые скобки, употребление, 188 л Литералы применение, 146 разнотипные, применение, 145 смысловое значение, 145 строковые, применение, 226 целочисленные, обозначение, 172 м Методы clone() небезопасное применение, 50 корректное применение, 233 equals () назначение, 220 небезопасное применение, 52 intern(), применение, 222 ordinal(), назначение, 115 super.clone(), правильный вызов, 196 возврат пустого массива или коллекции, 150 делегирование реализации подклассам, 135 загрузка классов, 71 нестрогая проверка безопасности и полномочий, 71 обратного вызова, применение, 86 перегрузка, особенности, 205; 229 переопределение, в ненадежном коде, 60 с переменным количеством аргументов неоднозначность перегрузки, 175 объявление параметров, 122 определение, 121 употребление типов, 123 уязвимые, проверка аргументов, 32 н Надежность кода на Java, рекомендации, 140 определение, 139 программ, требования, 11 программного обеспечения, особая роль, 139 Неиспользуемые значения, выявление, 199 Непроверяемые предупреждения выдача и подавление, 94 разновидности, 94 О Область действия аннотаций, ограничение, 94 переменных, минимизация, 92 повторно используемые идентификаторы, 140 Оболочки для уязвимых классов, применение, 30 Обратные вызовы конечные, 89 локальные, 88 назначение, 85 применение, 85 Объекты контейрные, с длительным сроком действия, 167 контроль срока действия, 239 копирование, 233 проверка на равенство, 223 сериализация и десериализация, 235 с коротким сроком действия, 167 ссылки изменяемые и неизменяемые, публикация, 210 определение, 210 Операторы switch, особенности составления, 184 try с ресурсами, применение, 154 вычисления остатка, применение, 219 логические и поразрядные, различение, 223 присваивания в условных выражениях, 179 составные, 118 равенства, назначение, 220 условные ? общая форма и принцип действия, 158 определение результирующего типа, 159 правая ассоциативность, 159 утверждения assert, применение, 157 п Пароли хешируемые, дополнение затравкой, 55 хранение открытым текстом, 54 с помощью хеш-функций, 55 Переменные инициализация, особенности, 142 класса, объявление, 145 область действия, минимизация, 92 объявление, особенности, 142 Перечисления перестановка констант, 116 присоединение значимости к порядковому значению констант, ошибки, 115 Полномочия ограничения на предоставление, 78 превышение, 78 специальные, предоставление, 78 Понятность программы, определение, 171 Предшествование операций, правила, 188 Приведение типов, 118 Привилегии наименьшие нарушение принципа, 67 соблюдение принципа, 68 особенности предоставления, 66 Привилегированный код, особенности применения, 69 Программирование на Java безопасное рекомендации, 12; 24 нормы и правила, 12 защитное общий принцип, 91 определение, 91
256 Предметный указатель понятное рекомендации,171 преимущества, 171 Продвижение типов примеры, 117 числовое назначение, 117 правила, 117 Р Рекомендации по программированию на Java назначение, 12 незатрагиваемые вопросы, 14 обеспечение безопасности, 13; 24 потенциальный круг специалистов, 15 применимость для платформы Android, 241 примеры ошибочного проектирования, 13 согласованная структура, 16 учет языковых особенностей, 13 Ресурсы закрываемые, управление, 156 системные, сериализация и десериализация описателей, 162 с Санобработка вводимых и выводимых данных, 36 данных по белому списку, 37 Символы в уникоде, 172 дезориентирующие, 171 управляющие, правильное употребление, 226 Символьные строки интернирование, 222 сравнение, 221 сцепление, 153 Синхронизация взаимодействия потоков, семантика, 216 доступа к данным, 211 операций чтения и записи, 212 Система сборки мусора в Java исключение пула объектов, 136 молодого и долгоживущего поколений объектов, 136 назначение, 135 особенности вызова, 136 сокращение затрат, 136 уязвимости, 135 Словарь специальных терминов, 245 Случайные числа, применимость, 60 Сокрытие, определение, 140 Ссылки изменчивые, объявление, 210 на объекты, объявление, 231 неизменяемые, объявление, 231 Ссылочное равенство определение, 220 принцип действия, 221 У Утверждения как ценное диагностическое средство, 158 назначение, 157 применение, 157 случаи непригодности, 157 Уязвимости CERT №636312, 74 CVE-2012-0507, 51 CVE-2012-4681,76 CVE-2013-0422, 76-77 GERONIMO-4574, 137 отказ в обслуживании, 149 связанные с файлами, 109 системы сборки мусора, 135 Уязвимые данные нарушение конфиденциальности, 24 ограничение срока действия, 24 сохранение в cookie-файле, 28 хранение на сервере, Т1 чтение из файла, 26 Ф Файлы ограничения на выгрузку по типам, 34 открытие и закрытие, 113 правил защиты полномочия, 80 специальные и дополнительные, 83 распознавание, 109 создание, особенности, 191 Фигурные скобки в условном операторе if, применение, 181 назначение, 181 X Хеш-функции назначение, 55 реализация, 55 хеш-значения, 55 ц Целочисленные значения, особенности преобразования,192 Циклические зависимости пакетов, преимущества исключения, 126 Циклы бесконечные, исключение, 186 правильное задание условий завершения, 187 ш Шаблон проектирования, “Пустой объект”, 168 Я Язык Java загрузка классов, механизм, 23 ложные представления программистов, 209 механизм защиты, 23 система сборки мусора, механизм, 135 строго типизированный, 23
“Каждый разработчик несет ответственность за авторство кода, свободного от уязвимостей в защите. В этой книге представлены реалистичные рекомендации, помогающие разработчикам реализовывать требуемые функциональные возможности для обеспечения безопасности, надежности и сопровождения разрабатываемых программ”. Мэри Энн Дэвидсон, начальник службы информационной безопасности компании Oracle Во многих организациях во всем мире программы на Java применяются для решения критически важных задач, а следовательно, их исходный код должен быть надежным, безопасным, быстрым и удобным для сопровождения, В рекомендациях, представленных в этой книге, собран практический опыт и примеры программирования на Java, помогающие удовлетворять потребности разработчиков. Эта книга, написанная по такому же образцу, как и справочное руководство The CERT® Oracle® Secure Coding Standard for Java ™, служит его расширением, направленным на решение многих вопросов повышения безопасности и качества исходного кода на Java. В книге представлены 75 рекомендаций в согласованной и понятной форме. Для каждой рекомендации указаны условия соответствия, приведены примеры кода, не соответствующего принятым нормам программирования на Java, а также представлены решения, соответствующие принятым нормам. Авторы книги доходчиво поясняют, когда именно следует применять каждую рекомендацию, а также дают ссылки на дополнительные источники информации. Отражая передовой опыт в области обеспечения безопасности программ на Java, это справочное руководство предоставляет усовершенствованные методики защиты подобных программ от злонамеренных атак и прочих неожиданных явлений. Читатель получает возможность ознакомиться с передовыми методиками повышения надежности и ясности исходного кода, а также с типичными ложными представлениями программирующих на Java, которым посвящена отдельная глава книги и которые нередко приводят к написанию неоптимального кода. ФРЭД ЛОНГ — преподаватель кафедры вычислительной техники в Университете Аберистуита, Великобритания. Он посещает Институт программотехники в США с 1992 года, тесно сотрудничая с его учеными ДХРУВ МОХИНДРА — ведущий специалист в группе, подчиненной руководителю технического отдела компании Persistent Systems Limited, India, где он консультирует по вопросам информационной безопасности в самых разных сферах деятельности, включая глобальную сеть, банковское дело и финансы, кооперацию, телекоммуникации, промышленные предприятия, мобильную связь, науки о живой природе и здравоохранение. РОБЕРТ С. СИКОРД — автор нескольких книг по компьютерной безопасности и программотехнике, а также технический руководитель по безопасному программированию в отделе CERT Института программотехники (SEI) Карнеги-Меллона в г. Питтсбург, шт. Пенсильвания ДИН Ф. САЗЕРЛЕНД — старший инженер по безопасности программного обеспечения в организации CERT, а прежде он занимался оптимизацией компиляторов в компании Tartan, Inc. ДЭВИД СВОБОДА — инженер по безопасности программного обеспечения в отделе CERT Института программотехники (SEI) Карнеги-Меллона, где он участвовал в качестве ведущего разработчика программного обеспечения в различных проектах данной организации. Категория: программирование Предмет рассмотрения: язык Java, нерсия SE 7 Уровень: промежуточный/опытный Издательский дом 'Вильямс* www.wilhamspublishing com Л Addison-Wesley informit.com/seiseries mformit.com/aw cert.org/books/java-codmg-guidelines