Skip to content

Commit 6425254

Browse files
committed
feat: enforce web UI access control on index, auth, and users routes using ipWhitelistOnly and webUiGuard middleware
1 parent 6660845 commit 6425254

File tree

6 files changed

+89
-7
lines changed

6 files changed

+89
-7
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,5 @@ COS_BUCKET_NAME=
105105
COS_REGION=
106106
COS_DOWNLOAD_URL=
107107

108+
# The value should be a comma-separated list of IP addresses, e.g., "127.0.0.1,10.0.0.1".
109+
WEB_UI_WHITELIST=

src/app.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import { logger } from 'kv-logger';
88
import { AppError, NotFound } from './core/app-error';
99
import { config } from './core/config';
1010
import { i18n } from './core/i18n';
11-
import { i18nMiddleware, Req, Res, withLogger } from './core/middleware';
11+
import {
12+
i18nMiddleware,
13+
ipWhitelistOnly,
14+
Req,
15+
Res,
16+
webUiGuard,
17+
withLogger,
18+
} from './core/middleware';
1219
import { accessKeysRouter } from './routes/accessKeys';
1320
import { accountRouter } from './routes/account';
1421
import { appsRouter } from './routes/apps';
@@ -89,8 +96,8 @@ app.use('/accessKeys', accessKeysRouter);
8996
app.use('/apps', appsRouter);
9097
app.use('/account', accountRouter);
9198
// code-push-server routes
92-
app.use('/auth', authRouter);
93-
app.use('/users', usersRouter);
99+
app.use('/auth', [ipWhitelistOnly, webUiGuard], authRouter);
100+
app.use('/users', [ipWhitelistOnly, webUiGuard], usersRouter);
94101

95102
// 404 handler
96103
// eslint-disable-next-line @typescript-eslint/no-unused-vars

src/core/config.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
withLogLevelFilter,
88
} from 'kv-logger';
99

10+
const env = process.env.NODE_ENV || 'development';
11+
1012
function toBool(str: string): boolean {
1113
return str === 'true' || str === '1';
1214
}
@@ -125,8 +127,19 @@ export const config = {
125127
updateCheckCache: toBool(process.env.UPDATE_CHECK_CACHE),
126128
// options value is (true | false), when it's true, it will cache rollout results in redis
127129
rolloutClientUniqueIdCache: toBool(process.env.ROLLOUT_CLIENT_UNIQUE_ID_CACHE),
130+
// NODE_ENV value. This determines the current running environment (e.g., development, staging, production).
131+
env,
132+
/**
133+
* whitelist for allowing access to the web UI.
134+
* When in production or when registration is disabled, only IPs listed here will be allowed to access the web UI.
135+
* The value should be a comma-separated list of IP addresses, e.g., "127.0.0.1,10.0.0.1".
136+
*/
137+
webUIWhitelist: (process.env.WEB_UI_WHITELIST || '')
138+
.split(',')
139+
.map((ip) => ip.trim())
140+
.filter(Boolean),
128141
},
129-
// Config for smtp emailregister module need validate user email project source https://github.com/nodemailer/nodemailer
142+
// Config for smtp email, register module need validate user email project source https://github.com/nodemailer/nodemailer
130143
smtpConfig: {
131144
host: process.env.SMTP_HOST,
132145
port: toNumber(process.env.SMTP_PORT, 465),
@@ -153,7 +166,6 @@ setLogTransports(
153166
),
154167
);
155168

156-
const env = process.env.NODE_ENV || 'development';
157169
logger.info(`use config`, {
158170
env,
159171
storageType: config.common.storageType,

src/core/middleware.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Users, UsersInterface } from '../models/users';
1010
import { AppError, Unauthorized } from './app-error';
1111
import { config } from './config';
1212
import { t } from './i18n';
13+
import { shouldHideWebUI } from './utils/common';
1314
import { parseToken, md5 } from './utils/security';
1415

1516
export type LocaleI18n = 'en' | 'ko' | 'zh';
@@ -164,3 +165,49 @@ export function i18nMiddleware(req: Req, res: Res, next: NextFunction) {
164165

165166
next();
166167
}
168+
169+
export function webUiGuard(req: Req, res: Response, next: NextFunction) {
170+
if (!shouldHideWebUI()) {
171+
return next();
172+
}
173+
174+
req.logger?.info?.('blocked web UI access', {
175+
path: req.path,
176+
method: req.method,
177+
});
178+
179+
return res.status(405).send('Method Not Allowed');
180+
}
181+
182+
export function ipWhitelistOnly(req: Req, res: Response, next: NextFunction) {
183+
const whitelist = config.common.webUIWhitelist || [];
184+
185+
if (!Array.isArray(whitelist) || whitelist.length === 0) {
186+
return next();
187+
}
188+
189+
const defaultIp = req.ip;
190+
const forwardedFor = req.headers['x-forwarded-for'];
191+
192+
const realIp = Array.isArray(forwardedFor)
193+
? forwardedFor[0]
194+
: forwardedFor?.split(',')[0]?.trim() || defaultIp;
195+
196+
const isAllowed = whitelist.includes(realIp);
197+
198+
if (!isAllowed) {
199+
req.logger?.info?.('IP whitelist blocked', {
200+
requestIp: realIp,
201+
path: req.path,
202+
});
203+
204+
return res.status(403).send('Forbidden');
205+
}
206+
207+
req.logger?.info?.('IP whitelist allowed', {
208+
requestIp: realIp,
209+
path: req.path,
210+
});
211+
212+
return next();
213+
}

src/core/utils/common.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ import { config } from '../config';
1313

1414
const streamPipeline = util.promisify(pipeline);
1515

16+
/** 프로덕션이거나, ALLOW_REGISTRATION !== true 이면 UI 숨김 */
17+
export function shouldHideWebUI(): boolean {
18+
const env = config.common.env || process.env.NODE_ENV;
19+
const isProdEnv = env === 'production';
20+
const allowRegistration = Boolean(config.common.allowRegistration);
21+
22+
return isProdEnv || !allowRegistration;
23+
}
24+
1625
function cleanVersion(versionNo?: string) {
1726
if (typeof versionNo !== 'string') {
1827
return versionNo as any;

src/routes/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import express from 'express';
22
import { AppError } from '../core/app-error';
33
import { i18n } from '../core/i18n';
4-
import { checkToken, Req } from '../core/middleware';
4+
import { checkToken, ipWhitelistOnly, Req, webUiGuard } from '../core/middleware';
55
import { clientManager } from '../core/services/client-manager';
66

77
export const indexRouter = express.Router();
88

9-
indexRouter.get('/', (req, res) => {
9+
/**
10+
* 프로덕션 레벨 미들웨어 적용
11+
* - ipWhitelistOnly: IP주소 검사
12+
* - webUiGuard: NODE_ENV='production' OR ALLOW_REGISTRATION='true'
13+
*/
14+
indexRouter.get('/', [ipWhitelistOnly, webUiGuard], (req, res) => {
1015
res.render('index', { title: 'CodePushServer' });
1116
});
1217

0 commit comments

Comments
 (0)