Skip to content

Commit c94de3c

Browse files
Fix security vulnerabilities and improve LOG_IGNORE_PATH
- Update express to ^4.22.0 to fix body-parser and qs vulnerabilities - Add npm override for jws ^3.2.3 to fix CVE-2025-65945 - Generate SSL certificates in memory at runtime instead of build time to avoid storing private keys in the Docker image - Make LOG_IGNORE_PATH also skip Morgan HTTP access logs, not just the JSON echo output - Update LOG_WITHOUT_NEWLINE test to expect 4 log lines (includes the certificate generation message)
1 parent 56c2e0f commit c94de3c

File tree

5 files changed

+343
-149
lines changed

5 files changed

+343
-149
lines changed

Dockerfile

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,10 @@ COPY . /app
66
RUN set -ex \
77
# Build JS-Application
88
&& npm install --production \
9-
# Generate SSL-certificate (for HTTPS)
10-
&& apk --no-cache add openssl \
11-
&& openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout privkey.pem -out fullchain.pem \
12-
-subj "/C=GB/ST=London/L=London/O=Mendhak/CN=my.example.com" \
13-
-addext "subjectAltName=DNS:my.example.com,DNS:my.example.net,IP:192.168.50.108,IP:127.0.0.1" \
14-
&& apk del openssl \
15-
&& rm -rf /var/cache/apk/* \
169
# Delete unnecessary files
1710
&& rm package* \
1811
# Correct User's file access
19-
&& chown -R node:node /app \
20-
&& chmod +r /app/privkey.pem
12+
&& chown -R node:node /app
2113

2214
FROM node:22-alpine AS final
2315
LABEL \

index.js

Lines changed: 215 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
const os = require('os');
2+
const fs = require('fs');
3+
const crypto = require('crypto');
24
const jwt = require('jsonwebtoken');
35
const http = require('http')
46
const https = require('https')
@@ -9,6 +11,207 @@ const { promisify } = require('util');
911
const promBundle = require("express-prom-bundle");
1012
const zlib = require("zlib");
1113

14+
// Get HTTPS credentials - either from files or generate self-signed in memory
15+
function getHttpsCredentials() {
16+
const keyFile = process.env.HTTPS_KEY_FILE;
17+
const certFile = process.env.HTTPS_CERT_FILE;
18+
19+
// If both files are specified and exist, use them
20+
if (keyFile && certFile) {
21+
try {
22+
return {
23+
key: fs.readFileSync(keyFile),
24+
cert: fs.readFileSync(certFile)
25+
};
26+
} catch (err) {
27+
console.log(`Could not read cert files (${err.message}), generating self-signed certificate...`);
28+
}
29+
}
30+
31+
// Try default file locations for backward compatibility
32+
try {
33+
return {
34+
key: fs.readFileSync('privkey.pem'),
35+
cert: fs.readFileSync('fullchain.pem')
36+
};
37+
} catch (err) {
38+
// Generate self-signed certificate in memory
39+
console.log('Generating self-signed certificate in memory...');
40+
return generateSelfSignedCertificate();
41+
}
42+
}
43+
44+
// Generate a self-signed certificate entirely in memory
45+
function generateSelfSignedCertificate() {
46+
const { privateKey } = crypto.generateKeyPairSync('rsa', {
47+
modulusLength: 2048,
48+
publicKeyEncoding: { type: 'spki', format: 'pem' },
49+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
50+
});
51+
52+
const publicKey = crypto.createPublicKey(crypto.createPrivateKey(privateKey));
53+
54+
// Get the public key in DER format for the certificate
55+
const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' });
56+
57+
// Create certificate structure
58+
const serialNumber = crypto.randomBytes(8);
59+
const now = new Date();
60+
const notBefore = now;
61+
const notAfter = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
62+
63+
// Build ASN.1 TBSCertificate
64+
const tbsCert = buildTBSCertificate(serialNumber, notBefore, notAfter, publicKeyDer);
65+
66+
// Sign the TBSCertificate
67+
const sign = crypto.createSign('SHA256');
68+
sign.update(tbsCert);
69+
const signature = sign.sign(privateKey);
70+
71+
// Build the full certificate
72+
const cert = buildCertificate(tbsCert, signature);
73+
74+
// Convert to PEM
75+
const certBase64 = cert.toString('base64');
76+
const certPem = '-----BEGIN CERTIFICATE-----\n' +
77+
certBase64.match(/.{1,64}/g).join('\n') +
78+
'\n-----END CERTIFICATE-----\n';
79+
80+
return { key: privateKey, cert: certPem };
81+
}
82+
83+
// ASN.1 DER encoding helpers
84+
function encodeLength(len) {
85+
if (len < 128) return Buffer.from([len]);
86+
if (len < 256) return Buffer.from([0x81, len]);
87+
if (len < 65536) return Buffer.from([0x82, (len >> 8) & 0xff, len & 0xff]);
88+
throw new Error('Length too long');
89+
}
90+
91+
function encodeSequence(contents) {
92+
const len = encodeLength(contents.length);
93+
return Buffer.concat([Buffer.from([0x30]), len, contents]);
94+
}
95+
96+
function encodeInteger(buf) {
97+
// Add leading zero if high bit is set
98+
if (buf[0] & 0x80) {
99+
buf = Buffer.concat([Buffer.from([0x00]), buf]);
100+
}
101+
const len = encodeLength(buf.length);
102+
return Buffer.concat([Buffer.from([0x02]), len, buf]);
103+
}
104+
105+
function encodeOID(oid) {
106+
const parts = oid.split('.').map(Number);
107+
const bytes = [parts[0] * 40 + parts[1]];
108+
for (let i = 2; i < parts.length; i++) {
109+
let val = parts[i];
110+
if (val < 128) {
111+
bytes.push(val);
112+
} else {
113+
const encoded = [];
114+
while (val > 0) {
115+
encoded.unshift((val & 0x7f) | (encoded.length ? 0x80 : 0));
116+
val >>= 7;
117+
}
118+
bytes.push(...encoded);
119+
}
120+
}
121+
const buf = Buffer.from(bytes);
122+
return Buffer.concat([Buffer.from([0x06]), encodeLength(buf.length), buf]);
123+
}
124+
125+
function encodeUTCTime(date) {
126+
const str = date.toISOString().replace(/[-:T]/g, '').slice(2, 14) + 'Z';
127+
const buf = Buffer.from(str, 'ascii');
128+
return Buffer.concat([Buffer.from([0x17]), encodeLength(buf.length), buf]);
129+
}
130+
131+
function encodePrintableString(str) {
132+
const buf = Buffer.from(str, 'ascii');
133+
return Buffer.concat([Buffer.from([0x13]), encodeLength(buf.length), buf]);
134+
}
135+
136+
function encodeSet(contents) {
137+
const len = encodeLength(contents.length);
138+
return Buffer.concat([Buffer.from([0x31]), len, contents]);
139+
}
140+
141+
function encodeBitString(buf) {
142+
// Prepend with 0x00 to indicate no unused bits
143+
const content = Buffer.concat([Buffer.from([0x00]), buf]);
144+
return Buffer.concat([Buffer.from([0x03]), encodeLength(content.length), content]);
145+
}
146+
147+
function buildRDN(oid, value) {
148+
const attrType = encodeOID(oid);
149+
const attrValue = encodePrintableString(value);
150+
const attrTypeAndValue = encodeSequence(Buffer.concat([attrType, attrValue]));
151+
return encodeSet(attrTypeAndValue);
152+
}
153+
154+
function buildName() {
155+
// CN=my.example.com,O=Mendhak,L=London,ST=London,C=GB
156+
const cn = buildRDN('2.5.4.3', 'my.example.com'); // commonName
157+
const o = buildRDN('2.5.4.10', 'Mendhak'); // organizationName
158+
const l = buildRDN('2.5.4.7', 'London'); // localityName
159+
const st = buildRDN('2.5.4.8', 'London'); // stateOrProvinceName
160+
const c = buildRDN('2.5.4.6', 'GB'); // countryName
161+
return encodeSequence(Buffer.concat([c, st, l, o, cn]));
162+
}
163+
164+
function buildValidity(notBefore, notAfter) {
165+
return encodeSequence(Buffer.concat([
166+
encodeUTCTime(notBefore),
167+
encodeUTCTime(notAfter)
168+
]));
169+
}
170+
171+
function buildAlgorithmIdentifier() {
172+
// sha256WithRSAEncryption
173+
const oid = encodeOID('1.2.840.113549.1.1.11');
174+
const params = Buffer.from([0x05, 0x00]); // NULL
175+
return encodeSequence(Buffer.concat([oid, params]));
176+
}
177+
178+
function buildTBSCertificate(serialNumber, notBefore, notAfter, publicKeyDer) {
179+
// Version (v3 = 2)
180+
const version = Buffer.concat([
181+
Buffer.from([0xa0, 0x03, 0x02, 0x01, 0x02])
182+
]);
183+
184+
const serial = encodeInteger(serialNumber);
185+
const signatureAlg = buildAlgorithmIdentifier();
186+
const issuer = buildName();
187+
const validity = buildValidity(notBefore, notAfter);
188+
const subject = buildName();
189+
190+
// SubjectPublicKeyInfo is already in DER format
191+
const subjectPublicKeyInfo = publicKeyDer;
192+
193+
return encodeSequence(Buffer.concat([
194+
version,
195+
serial,
196+
signatureAlg,
197+
issuer,
198+
validity,
199+
subject,
200+
subjectPublicKeyInfo
201+
]));
202+
}
203+
204+
function buildCertificate(tbsCert, signature) {
205+
const signatureAlg = buildAlgorithmIdentifier();
206+
const signatureValue = encodeBitString(signature);
207+
208+
return encodeSequence(Buffer.concat([
209+
tbsCert,
210+
signatureAlg,
211+
signatureValue
212+
]));
213+
}
214+
12215
const {
13216
PROMETHEUS_ENABLED = false,
14217
PROMETHEUS_METRICS_PATH = '/metrics',
@@ -40,7 +243,15 @@ if(PROMETHEUS_ENABLED === 'true') {
40243
}
41244

42245
if(process.env.DISABLE_REQUEST_LOGS !== 'true'){
43-
app.use(morgan('combined'));
246+
app.use(morgan('combined', {
247+
skip: function (req, res) {
248+
// Skip logging for paths matching LOG_IGNORE_PATH
249+
if (process.env.LOG_IGNORE_PATH && new RegExp(process.env.LOG_IGNORE_PATH).test(req.path)) {
250+
return true;
251+
}
252+
return false;
253+
}
254+
}));
44255
}
45256

46257
app.use(function(req, res, next){
@@ -197,9 +408,10 @@ let httpOpts = {
197408
maxHeaderSize: maxHeaderSize
198409
}
199410

411+
const httpsCredentials = getHttpsCredentials();
200412
let httpsOpts = {
201-
key: require('fs').readFileSync(process.env.HTTPS_KEY_FILE || 'privkey.pem'),
202-
cert: require('fs').readFileSync(process.env.HTTPS_CERT_FILE || 'fullchain.pem'),
413+
key: httpsCredentials.key,
414+
cert: httpsCredentials.cert,
203415
maxHeaderSize: maxHeaderSize
204416
};
205417

0 commit comments

Comments
 (0)