xls-to-xlsx
v1.2.3
Published
Secure XLS → XLSX converter (also CSV/ODS/JSON). Pure-JS helpers + a tiny client SDK + a hardened, CVE-scanned Docker image.
Maintainers
Readme
XLS to XLSX Converter
Secure XLS/XLSX/CSV/ODS/JSON conversion. Ships as both an npm package (for use as a JS library or client SDK) and a Docker image (for the full Python + Node service).
docker scout cves reported 0 across every severity for the 1.2.2 image
on 2026-07-31. That is a measurement with a date on it, not a standing
property: see Staying at zero for why it decays and how to
re-check it yourself.
🚀 Overview
The XLS to XLSX Converter is a modern solution for converting various spreadsheet formats (XLS, CSV, ODS, JSON) to the XLSX format. It features a Python-based core conversion service with JavaScript wrappers and interfaces, addressing the historical pain points of handling XLS files in JavaScript environments by providing a reliable, secure, and efficient conversion service.
Why it Exists
Converting XLS files in JavaScript environments has been traditionally challenging due to:
- Lack of Official Specifications: The binary XLS/BIFF formats lack public specifications, making implementation difficult
- JavaScript Library Limitations: As confirmed by SheetJS documentation, "There is no official specification for any of these [BIFF2-5] formats," forcing JS libraries to rely on reverse-engineering with inconsistent results
- Browser Limitations: Browsers have limited capability to handle binary compound file formats (CFB)
- Edge Cases: Many existing libraries have significant limitations with older XLS versions or complex spreadsheets
- Security Concerns: Formula-based attacks and insufficient validation in many converters
Architecture Approach
This project takes a hybrid approach to solve these challenges:
- Python Core Service: Leverages Python's robust pandas+xlrd ecosystem for reliable parsing of legacy XLS formats
- JavaScript Wrappers: Provides clean JS/Node.js wrappers and interfaces for web applications
- Microservice Design: Deployed as a standalone service or integrated directly in applications
This architecture provides a production-grade solution to these challenges, combining the reliability of Python's mature XLS parsing libraries with clean JavaScript interfaces and robust security measures.
✨ Features
- Multiple Format Support: Convert from XLS, XLSX, CSV, ODS, and JSON to XLSX
- Security Focused: Automatic formula stripping to prevent security vulnerabilities
- Browser & Node.js Support: Use either as a service or directly in the browser
- Comprehensive Testing: Validated against real-world edge cases
- Authentication: JWT-based authentication for API endpoints
- Interactive Demo: Browser-based demo for easy testing
- API & Client Libraries: Clean interfaces for integration
📋 Installation & Setup
Prerequisites
- Node.js 18+ (for the bundled
fetchand modern ESM) - Python 3.8+ with pip
- Git
Server-Side Setup
git clone https://github.com/helloderekg/xls-converter.git
cd xls-converter
npm install
pip install -r requirements.txt
# Start both services together
npm startnpm start runs start-services.js, which spawns the
Python conversion service and the Node API gateway and forwards their logs.
Starting individual components
npm run start:python # Flask conversion service on :5001
npm run start:node # Node API gateway on :4040
npm run start:client # Static demo client on :4001 (python http.server)Docker Deployment
The project ships a container image built on
Chainguard's Wolfi base, with Python and
Node.js bundled in a single hardened image. On 2026-07-31 docker scout cves
reported 0 critical / 0 high / 0 medium / 0 low / 0 unspecified for the
1.2.2 build, across 289 indexed packages (149 MB).
Docker Hub: https://hub.docker.com/r/derekgsayshi/xls-converter
# Pull
docker pull derekgsayshi/xls-converter:1.2.1
# Run (Node API on 4040, Python on 5001, demo client on 4001)
docker run --rm \
-p 4040:4040 -p 5001:5001 -p 4001:4001 \
derekgsayshi/xls-converter:1.2.1Or build it yourself:
npm run docker:build # builds :1.2.1 and :latest
npm run docker:run # runs the image with default port mapping
npm run docker:scan # runs `docker scout cves` against the imageThe container starts three services on startup:
- Python XLS conversion service -
http://localhost:5001(internal) - Node API gateway -
http://localhost:4040(/health,/convert,/cors-test) - Static web client -
http://localhost:4001
Environment variables
| Variable | Default | Used by | Purpose |
|----------------------------|---------|---------|------------------------------------------------------|
| PORT | 4040 | Node | Public API gateway port |
| PYTHON_SERVICE_PORT | 5001 | Python | Internal conversion service port |
| CLIENT_PORT | 4001 | static | Web demo port |
| SECRET_KEY | dev key | both | JWT secret. Must be the same in Node and Python. |
| REQUIRE_AUTH | false | Node | Require Bearer JWT for POST /convert |
| XLS_CONVERSION_SERVICE_URL | http://localhost:5001 | Node | Where to forward conversion requests |
Environment Variables
When using Docker, you can customize ports using environment variables:
PORT: Test server port (default: 4040)PYTHON_SERVICE_PORT: Python service port (default: 5001)CLIENT_PORT: Web client port (default: 4001)
Example with custom ports:
docker run -e PORT=8080 -e PYTHON_SERVICE_PORT=8081 -e CLIENT_PORT=8082 -p 8080:8080 -p 8081:8081 -p 8082:8082 xls-converter🛠️ Usage
There are three ways to use this project, depending on what you actually need:
Mode 1 — Pure-JS library (no Python required)
For building XLSX files from JS data, or stripping formulas from existing workbooks. These helpers run anywhere ExcelJS runs.
npm install xls-to-xlsximport { createXlsxBuffer, stripFormulas } from 'xls-to-xlsx';
// Build an XLSX from an array of objects
const buffer = await createXlsxBuffer([
{ name: 'Alice', score: 91 },
{ name: 'Bob', score: 87 },
]);
fs.writeFileSync('out.xlsx', Buffer.from(buffer));
// Or harden an existing ExcelJS workbook against formula-injection
import ExcelJS from 'exceljs';
const wb = new ExcelJS.Workbook();
await wb.xlsx.readFile('untrusted.xlsx');
stripFormulas(wb);
await wb.xlsx.writeFile('safe.xlsx');Mode 2 — Client SDK against a running converter service
Use this when you need to convert legacy .xls (or .csv/.ods) files —
that path needs the Python service running somewhere. The simplest way to
get a service is to docker run the image (see Mode 3 below).
import fs from 'node:fs';
import { XlsConverterClient } from 'xls-to-xlsx';
const client = new XlsConverterClient('http://localhost:4040');
// Health check
console.log(await client.health()); // → { status: 'ok', timestamp: ... }
// Convert a file
const xlsxBytes = await client.convert(
fs.readFileSync('legacy.xls'),
{ filename: 'legacy.xls', contentType: 'application/vnd.ms-excel' }
);
fs.writeFileSync('legacy.xlsx', Buffer.from(xlsxBytes));In a browser:
import { XlsConverterClient } from 'xls-to-xlsx/client';
const client = new XlsConverterClient('https://your-converter.example');
const file = document.querySelector('input[type=file]').files[0];
const xlsxBytes = await client.convert(file);
const url = URL.createObjectURL(new Blob([xlsxBytes]));
const a = document.createElement('a');
a.href = url;
a.download = file.name.replace(/\.[^.]+$/, '.xlsx');
a.click();If the server has REQUIRE_AUTH=true, pass a Bearer JWT:
const client = new XlsConverterClient('https://your-converter.example', {
token: yourSignedJwt,
});Mode 3 — Full service via Docker (Python + Node bundled)
The derekgsayshi/xls-converter image bundles the Python conversion engine,
the Node API gateway, and the static web demo in a single hardened container.
docker run --rm \
-p 4040:4040 -p 5001:5001 -p 4001:4001 \
derekgsayshi/xls-converter:1.2.1Then POST your file to http://localhost:4040/convert (or open the demo
at http://localhost:4001/).
🔒 Security
Application-level controls
- Formula Stripping: All formulas are removed from cells to prevent formula injection attacks
- File Size Limits: Default 50MB maximum file size to prevent DoS attacks
- MIME Type Validation: Strict validation of file types
- JWT Authentication: Protected API endpoints
- Input Sanitization: Proper handling of filenames and paths
Container hardening (v1.2.0)
- Clean scan at build time: Built on
cgr.dev/chainguard/wolfi-base, which is rebuilt nightly against the latest CVE fixes.docker scout cvesreported0C / 0H / 0M / 0L / 0 unspecifiedfor1.2.2on 2026-07-31. - No pip or setuptools at runtime: both are removed after the Python
dependencies are installed. Nothing at runtime uses them, and pip ships a
vendored-dependency SBOM (
pip/_vendor/bom.cdx.json) that scanners read as installed packages — that one file accounted for two findings for libraries the image never imports. - No build tools at runtime: No
gcc, nonpm, nocurlin the final image — onlypython,nodejs, andbusybox(which provideswgetfor the healthcheck). - Non-root user: The container runs as
appuser, not root. - Multi-stage build: Build dependencies (
build-base,npm) live only in the intermediate stage and are dropped from the final image. - Lockfile regenerated in build:
package-lock.jsonis excluded from the build context and regenerated against the currentoverrides, so vulnerability scanners read fresh resolved versions instead of stale lockfile entries. - Single source of truth for the Node server (new in 1.2.0): the old
test-server.js(which had a deadcreateExcelFilereference and was drifting away from the canonical server) was deleted. Both local dev and the Docker image now run src/server/index.js. - JWT env var unified (new in 1.2.0): Server↔Python auth uses
SECRET_KEYon both sides. Previously the Node side readJWT_SECRET_KEYand the Python side readSECRET_KEY— same default value, so it worked in dev, but would silently break in any prod deployment that set a custom secret.
📈 Performance
- Handles files up to 50MB
- Benchmarked performance on large datasets
- Optimized memory usage
📚 Documentation
For complete documentation, visit the docs folder
- API Reference
- Security Guide
- Browser Integration
- Usage Guide
- Comparison with other libraries
- Why this exists
🧪 Testing
Comprehensive tests are available in the test directory:
Run all tests
npm test
Run with coverage report
npm run test:coverage
The test suite includes:
- Unit Tests: Core functionality testing
- Integration Tests: Full API endpoint testing
- Real-world Files: Tests against a library of challenging XLS files
- Edge Cases: Handling of corrupted or unusual files
- Performance Tests: Benchmarks for large files
🔄 Related Solutions
This project was created after extensive research into existing solutions:
- SheetJS/xlsx: Comprehensive but with some limitations on older XLS formats
- exceljs: Strong XLSX support but limited XLS capabilities
- node-xlsx: Simple interface but lacks robust error handling
- And more: See our comparison document
🤝 Contributing
Contributions are welcome! Please see our contributing guidelines.
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Staying at zero
A clean scan is a fact about a build, not about the project. The 1.2.1 image
published on 2026-04-07 scanned clean at the time; re-scanned on 2026-07-31 it
reported 148 findings (9 critical, 52 high). Nothing about it changed — the
world moved and the image did not. Rebuilding the same Dockerfile the same day
produced 0, because Wolfi rebuilds its packages against current fixes.
So the image needs rebuilding and republishing on a schedule, not once. Check any tag yourself:
docker scout cves derekgsayshi/xls-converter:1.2.2 --platform linux/amd64If that returns findings, the fix is almost always docker build again rather
than a code change.
A note for npm consumers
npm overrides apply only to the project that declares them, so the pins in
this repo's package.json protect this repo's builds and the container image —
they do not follow the package into your project. Installing xls-to-xlsx
resolves [email protected] transitively through [email protected], which pins
uuid@^8.3.0 and has no newer release. If your own audit flags it, add the
override on your side:
{ "overrides": { "uuid": "11.1.1" } }Verified compatible: exceljs uses only const {v4} = require('uuid'), which
uuid 11 still provides, and a write/read round-trip through exceljs passes
under the override.
