nyte 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -1
- package/dist/security.selftest.d.ts +0 -1
- package/dist/security.selftest.js +0 -170
- package/src/security.selftest.ts +0 -192
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nyte",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Nyte.js is a high-level framework for building web applications with ease and speed. It provides a robust set of tools and features to streamline development and enhance productivity.",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"author": "itsmuzin",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"import": "./dist/index.js",
|
|
30
30
|
"require": "./dist/index.js"
|
|
31
31
|
},
|
|
32
|
+
"./console": {
|
|
33
|
+
"types": "./dist/api/console.d.ts",
|
|
34
|
+
"import": "./dist/api/console.js",
|
|
35
|
+
"require": "./dist/api/console.js"
|
|
36
|
+
},
|
|
32
37
|
"./react": {
|
|
33
38
|
"types": "./dist/client/client.d.ts",
|
|
34
39
|
"import": "./dist/client/client.js",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
* Lightweight security self-tests (no external test runner required).
|
|
4
|
-
*
|
|
5
|
-
* These tests validate that:
|
|
6
|
-
* - Static serving does NOT allow path traversal outside its base dirs.
|
|
7
|
-
* - HTTP->HTTPS redirect does NOT trust hostile Host headers.
|
|
8
|
-
*
|
|
9
|
-
* Run (from repo root):
|
|
10
|
-
* node packages/nytejs/dist/security.selftest.js
|
|
11
|
-
*/
|
|
12
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
-
};
|
|
15
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
const fs_1 = __importDefault(require("fs"));
|
|
17
|
-
const path_1 = __importDefault(require("path"));
|
|
18
|
-
const http_1 = __importDefault(require("http"));
|
|
19
|
-
const helpers_1 = require("./helpers");
|
|
20
|
-
function assert(condition, message) {
|
|
21
|
-
if (!condition)
|
|
22
|
-
throw new Error(message);
|
|
23
|
-
}
|
|
24
|
-
async function httpRequest(opts) {
|
|
25
|
-
return new Promise((resolve, reject) => {
|
|
26
|
-
const req = http_1.default.request(opts, (res) => {
|
|
27
|
-
let data = '';
|
|
28
|
-
res.setEncoding('utf8');
|
|
29
|
-
res.on('data', (c) => (data += c));
|
|
30
|
-
res.on('end', () => resolve({ status: res.statusCode || 0, headers: res.headers, body: data }));
|
|
31
|
-
});
|
|
32
|
-
req.on('error', reject);
|
|
33
|
-
if (opts.body)
|
|
34
|
-
req.write(opts.body);
|
|
35
|
-
req.end();
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
function randomPort() {
|
|
39
|
-
// Avoid privileged ports. Not cryptographic; just for tests.
|
|
40
|
-
return 20000 + Math.floor(Math.random() * 20000);
|
|
41
|
-
}
|
|
42
|
-
// --- Redirect host poisoning: test a pure builder (no TLS required) ---
|
|
43
|
-
function buildHttpsRedirectLocation(params) {
|
|
44
|
-
const rawHost = String(params.requestHostHeader || '').trim();
|
|
45
|
-
let hostToUse = String(params.configuredHostname || params.fallbackHostname || '').trim();
|
|
46
|
-
if (!hostToUse) {
|
|
47
|
-
const hostWithoutPort = rawHost.split(':')[0];
|
|
48
|
-
const isValidHost = /^[a-zA-Z0-9.-]+$/.test(hostWithoutPort) && !hostWithoutPort.includes('..');
|
|
49
|
-
hostToUse = isValidHost ? hostWithoutPort : 'localhost';
|
|
50
|
-
}
|
|
51
|
-
let redirectUrl = `https://${hostToUse}`;
|
|
52
|
-
if (params.httpsPort !== 443)
|
|
53
|
-
redirectUrl += `:${params.httpsPort}`;
|
|
54
|
-
redirectUrl += params.requestUrl || '/';
|
|
55
|
-
return redirectUrl;
|
|
56
|
-
}
|
|
57
|
-
async function withTempProject(fn) {
|
|
58
|
-
const base = path_1.default.join(process.cwd(), '.nyte-security-selftest');
|
|
59
|
-
fs_1.default.mkdirSync(base, { recursive: true });
|
|
60
|
-
const projectDir = fs_1.default.mkdtempSync(path_1.default.join(base, 'proj-'));
|
|
61
|
-
// minimal folder structure
|
|
62
|
-
fs_1.default.mkdirSync(path_1.default.join(projectDir, 'src', 'web', 'routes'), { recursive: true });
|
|
63
|
-
fs_1.default.mkdirSync(path_1.default.join(projectDir, 'src', 'backend', 'routes'), { recursive: true });
|
|
64
|
-
fs_1.default.mkdirSync(path_1.default.join(projectDir, 'public'), { recursive: true });
|
|
65
|
-
// Basic route so app.prepare() succeeds (route loader expects files)
|
|
66
|
-
const routeFile = path_1.default.join(projectDir, 'src', 'web', 'routes', 'index.tsx');
|
|
67
|
-
fs_1.default.writeFileSync(routeFile, `export default function Index(){ return null; }\nexport const component = Index;\n`, 'utf8');
|
|
68
|
-
// A secret file OUTSIDE public/.nyte to test traversal attempts
|
|
69
|
-
fs_1.default.writeFileSync(path_1.default.join(projectDir, 'SECRET.txt'), 'TOP_SECRET', 'utf8');
|
|
70
|
-
try {
|
|
71
|
-
return await fn(projectDir);
|
|
72
|
-
}
|
|
73
|
-
finally {
|
|
74
|
-
try {
|
|
75
|
-
fs_1.default.rmSync(projectDir, { recursive: true, force: true });
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// ignore cleanup issues
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
async function testStaticPathTraversal() {
|
|
83
|
-
await withTempProject(async (projectDir) => {
|
|
84
|
-
const port = randomPort();
|
|
85
|
-
const server = await (0, helpers_1.app)({ dir: projectDir, dev: true, port, hostname: '127.0.0.1' }).init();
|
|
86
|
-
try {
|
|
87
|
-
// Try to escape public dir
|
|
88
|
-
const res1 = await httpRequest({
|
|
89
|
-
hostname: '127.0.0.1',
|
|
90
|
-
port,
|
|
91
|
-
method: 'GET',
|
|
92
|
-
path: '/../SECRET.txt'
|
|
93
|
-
});
|
|
94
|
-
assert(res1.status !== 200, `Expected traversal from public to be blocked, got 200 with body: ${res1.body}`);
|
|
95
|
-
assert(!res1.body.includes('TOP_SECRET'), 'Traversal leaked secret content');
|
|
96
|
-
// Try to escape .nyte via /_nyte/
|
|
97
|
-
const res2 = await httpRequest({
|
|
98
|
-
hostname: '127.0.0.1',
|
|
99
|
-
port,
|
|
100
|
-
method: 'GET',
|
|
101
|
-
path: '/_nyte/../SECRET.txt'
|
|
102
|
-
});
|
|
103
|
-
assert(res2.status !== 200, `Expected traversal from /_nyte to be blocked, got 200 with body: ${res2.body}`);
|
|
104
|
-
assert(!res2.body.includes('TOP_SECRET'), 'Traversal via /_nyte leaked secret content');
|
|
105
|
-
// Encoded traversal
|
|
106
|
-
const res3 = await httpRequest({
|
|
107
|
-
hostname: '127.0.0.1',
|
|
108
|
-
port,
|
|
109
|
-
method: 'GET',
|
|
110
|
-
path: '/_nyte/%2e%2e/SECRET.txt'
|
|
111
|
-
});
|
|
112
|
-
assert(res3.status !== 200, `Expected encoded traversal to be blocked, got 200 with body: ${res3.body}`);
|
|
113
|
-
assert(!res3.body.includes('TOP_SECRET'), 'Encoded traversal leaked secret content');
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
async function testHttpsRedirectHostPoisoning() {
|
|
121
|
-
// configuredHostname should win over hostile Host header
|
|
122
|
-
const loc1 = buildHttpsRedirectLocation({
|
|
123
|
-
configuredHostname: 'example.com',
|
|
124
|
-
fallbackHostname: '0.0.0.0',
|
|
125
|
-
httpsPort: 8443,
|
|
126
|
-
requestHostHeader: 'evil.com',
|
|
127
|
-
requestUrl: '/login?x=1'
|
|
128
|
-
});
|
|
129
|
-
assert(loc1.startsWith('https://example.com:8443/'), `Expected configured hostname, got ${loc1}`);
|
|
130
|
-
assert(!loc1.includes('evil.com'), `Redirect trusted hostile Host header. Location=${loc1}`);
|
|
131
|
-
// without configured host, a valid host header is accepted
|
|
132
|
-
const loc2 = buildHttpsRedirectLocation({
|
|
133
|
-
httpsPort: 443,
|
|
134
|
-
requestHostHeader: 'good.example',
|
|
135
|
-
requestUrl: '/'
|
|
136
|
-
});
|
|
137
|
-
assert(loc2 === 'https://good.example/', `Expected host header to be used, got ${loc2}`);
|
|
138
|
-
// invalid host header should be rejected
|
|
139
|
-
const loc3 = buildHttpsRedirectLocation({
|
|
140
|
-
httpsPort: 443,
|
|
141
|
-
requestHostHeader: 'evil.com\r\nX-Evil: 1',
|
|
142
|
-
requestUrl: '/'
|
|
143
|
-
});
|
|
144
|
-
assert(loc3 === 'https://localhost/', `Expected invalid host to fallback to localhost, got ${loc3}`);
|
|
145
|
-
}
|
|
146
|
-
async function main() {
|
|
147
|
-
const results = [];
|
|
148
|
-
async function run(name, fn) {
|
|
149
|
-
try {
|
|
150
|
-
await fn();
|
|
151
|
-
results.push({ name, ok: true });
|
|
152
|
-
}
|
|
153
|
-
catch (e) {
|
|
154
|
-
results.push({ name, ok: false, error: e?.message || String(e) });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
await run('static path traversal', testStaticPathTraversal);
|
|
158
|
-
await run('https redirect host poisoning', testHttpsRedirectHostPoisoning);
|
|
159
|
-
const failed = results.filter(r => !r.ok);
|
|
160
|
-
for (const r of results) {
|
|
161
|
-
// eslint-disable-next-line no-console
|
|
162
|
-
console.log(`${r.ok ? 'PASS' : 'FAIL'} - ${r.name}${r.ok ? '' : `: ${r.error}`}`);
|
|
163
|
-
}
|
|
164
|
-
if (failed.length) {
|
|
165
|
-
process.exitCode = 1;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
// Only run when executed directly
|
|
169
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
170
|
-
main();
|
package/src/security.selftest.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Lightweight security self-tests (no external test runner required).
|
|
3
|
-
*
|
|
4
|
-
* These tests validate that:
|
|
5
|
-
* - Static serving does NOT allow path traversal outside its base dirs.
|
|
6
|
-
* - HTTP->HTTPS redirect does NOT trust hostile Host headers.
|
|
7
|
-
*
|
|
8
|
-
* Run (from repo root):
|
|
9
|
-
* node packages/nytejs/dist/security.selftest.js
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import fs from 'fs';
|
|
13
|
-
import path from 'path';
|
|
14
|
-
import http from 'http';
|
|
15
|
-
import { app } from './helpers';
|
|
16
|
-
|
|
17
|
-
function assert(condition: any, message: string): void {
|
|
18
|
-
if (!condition) throw new Error(message);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function httpRequest(opts: http.RequestOptions & { body?: string }): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> {
|
|
22
|
-
return new Promise((resolve, reject) => {
|
|
23
|
-
const req = http.request(opts, (res) => {
|
|
24
|
-
let data = '';
|
|
25
|
-
res.setEncoding('utf8');
|
|
26
|
-
res.on('data', (c) => (data += c));
|
|
27
|
-
res.on('end', () => resolve({ status: res.statusCode || 0, headers: res.headers, body: data }));
|
|
28
|
-
});
|
|
29
|
-
req.on('error', reject);
|
|
30
|
-
if (opts.body) req.write(opts.body);
|
|
31
|
-
req.end();
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function randomPort(): number {
|
|
36
|
-
// Avoid privileged ports. Not cryptographic; just for tests.
|
|
37
|
-
return 20000 + Math.floor(Math.random() * 20000);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// --- Redirect host poisoning: test a pure builder (no TLS required) ---
|
|
41
|
-
function buildHttpsRedirectLocation(params: {
|
|
42
|
-
configuredHostname?: string;
|
|
43
|
-
fallbackHostname?: string;
|
|
44
|
-
httpsPort: number;
|
|
45
|
-
requestHostHeader?: string;
|
|
46
|
-
requestUrl?: string;
|
|
47
|
-
}): string {
|
|
48
|
-
const rawHost = String(params.requestHostHeader || '').trim();
|
|
49
|
-
let hostToUse = String(params.configuredHostname || params.fallbackHostname || '').trim();
|
|
50
|
-
|
|
51
|
-
if (!hostToUse) {
|
|
52
|
-
const hostWithoutPort = rawHost.split(':')[0];
|
|
53
|
-
const isValidHost = /^[a-zA-Z0-9.-]+$/.test(hostWithoutPort) && !hostWithoutPort.includes('..');
|
|
54
|
-
hostToUse = isValidHost ? hostWithoutPort : 'localhost';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let redirectUrl = `https://${hostToUse}`;
|
|
58
|
-
if (params.httpsPort !== 443) redirectUrl += `:${params.httpsPort}`;
|
|
59
|
-
redirectUrl += params.requestUrl || '/';
|
|
60
|
-
return redirectUrl;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function withTempProject<T>(fn: (projectDir: string) => Promise<T>): Promise<T> {
|
|
64
|
-
const base = path.join(process.cwd(), '.nyte-security-selftest');
|
|
65
|
-
fs.mkdirSync(base, { recursive: true });
|
|
66
|
-
const projectDir = fs.mkdtempSync(path.join(base, 'proj-'));
|
|
67
|
-
|
|
68
|
-
// minimal folder structure
|
|
69
|
-
fs.mkdirSync(path.join(projectDir, 'src', 'web', 'routes'), { recursive: true });
|
|
70
|
-
fs.mkdirSync(path.join(projectDir, 'src', 'backend', 'routes'), { recursive: true });
|
|
71
|
-
fs.mkdirSync(path.join(projectDir, 'public'), { recursive: true });
|
|
72
|
-
|
|
73
|
-
// Basic route so app.prepare() succeeds (route loader expects files)
|
|
74
|
-
const routeFile = path.join(projectDir, 'src', 'web', 'routes', 'index.tsx');
|
|
75
|
-
fs.writeFileSync(
|
|
76
|
-
routeFile,
|
|
77
|
-
`export default function Index(){ return null; }\nexport const component = Index;\n`,
|
|
78
|
-
'utf8'
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
// A secret file OUTSIDE public/.nyte to test traversal attempts
|
|
82
|
-
fs.writeFileSync(path.join(projectDir, 'SECRET.txt'), 'TOP_SECRET', 'utf8');
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
return await fn(projectDir);
|
|
86
|
-
} finally {
|
|
87
|
-
try {
|
|
88
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
89
|
-
} catch {
|
|
90
|
-
// ignore cleanup issues
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function testStaticPathTraversal(): Promise<void> {
|
|
96
|
-
await withTempProject(async (projectDir) => {
|
|
97
|
-
const port = randomPort();
|
|
98
|
-
const server = await app({ dir: projectDir, dev: true, port, hostname: '127.0.0.1' }).init();
|
|
99
|
-
try {
|
|
100
|
-
// Try to escape public dir
|
|
101
|
-
const res1 = await httpRequest({
|
|
102
|
-
hostname: '127.0.0.1',
|
|
103
|
-
port,
|
|
104
|
-
method: 'GET',
|
|
105
|
-
path: '/../SECRET.txt'
|
|
106
|
-
});
|
|
107
|
-
assert(res1.status !== 200, `Expected traversal from public to be blocked, got 200 with body: ${res1.body}`);
|
|
108
|
-
assert(!res1.body.includes('TOP_SECRET'), 'Traversal leaked secret content');
|
|
109
|
-
|
|
110
|
-
// Try to escape .nyte via /_nyte/
|
|
111
|
-
const res2 = await httpRequest({
|
|
112
|
-
hostname: '127.0.0.1',
|
|
113
|
-
port,
|
|
114
|
-
method: 'GET',
|
|
115
|
-
path: '/_nyte/../SECRET.txt'
|
|
116
|
-
});
|
|
117
|
-
assert(res2.status !== 200, `Expected traversal from /_nyte to be blocked, got 200 with body: ${res2.body}`);
|
|
118
|
-
assert(!res2.body.includes('TOP_SECRET'), 'Traversal via /_nyte leaked secret content');
|
|
119
|
-
|
|
120
|
-
// Encoded traversal
|
|
121
|
-
const res3 = await httpRequest({
|
|
122
|
-
hostname: '127.0.0.1',
|
|
123
|
-
port,
|
|
124
|
-
method: 'GET',
|
|
125
|
-
path: '/_nyte/%2e%2e/SECRET.txt'
|
|
126
|
-
});
|
|
127
|
-
assert(res3.status !== 200, `Expected encoded traversal to be blocked, got 200 with body: ${res3.body}`);
|
|
128
|
-
assert(!res3.body.includes('TOP_SECRET'), 'Encoded traversal leaked secret content');
|
|
129
|
-
} finally {
|
|
130
|
-
await new Promise<void>((resolve) => (server as any).close(() => resolve()));
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function testHttpsRedirectHostPoisoning(): Promise<void> {
|
|
136
|
-
// configuredHostname should win over hostile Host header
|
|
137
|
-
const loc1 = buildHttpsRedirectLocation({
|
|
138
|
-
configuredHostname: 'example.com',
|
|
139
|
-
fallbackHostname: '0.0.0.0',
|
|
140
|
-
httpsPort: 8443,
|
|
141
|
-
requestHostHeader: 'evil.com',
|
|
142
|
-
requestUrl: '/login?x=1'
|
|
143
|
-
});
|
|
144
|
-
assert(loc1.startsWith('https://example.com:8443/'), `Expected configured hostname, got ${loc1}`);
|
|
145
|
-
assert(!loc1.includes('evil.com'), `Redirect trusted hostile Host header. Location=${loc1}`);
|
|
146
|
-
|
|
147
|
-
// without configured host, a valid host header is accepted
|
|
148
|
-
const loc2 = buildHttpsRedirectLocation({
|
|
149
|
-
httpsPort: 443,
|
|
150
|
-
requestHostHeader: 'good.example',
|
|
151
|
-
requestUrl: '/'
|
|
152
|
-
});
|
|
153
|
-
assert(loc2 === 'https://good.example/', `Expected host header to be used, got ${loc2}`);
|
|
154
|
-
|
|
155
|
-
// invalid host header should be rejected
|
|
156
|
-
const loc3 = buildHttpsRedirectLocation({
|
|
157
|
-
httpsPort: 443,
|
|
158
|
-
requestHostHeader: 'evil.com\r\nX-Evil: 1',
|
|
159
|
-
requestUrl: '/'
|
|
160
|
-
});
|
|
161
|
-
assert(loc3 === 'https://localhost/', `Expected invalid host to fallback to localhost, got ${loc3}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function main(): Promise<void> {
|
|
165
|
-
const results: Array<{ name: string; ok: boolean; error?: string }> = [];
|
|
166
|
-
|
|
167
|
-
async function run(name: string, fn: () => Promise<void>) {
|
|
168
|
-
try {
|
|
169
|
-
await fn();
|
|
170
|
-
results.push({ name, ok: true });
|
|
171
|
-
} catch (e: any) {
|
|
172
|
-
results.push({ name, ok: false, error: e?.message || String(e) });
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
await run('static path traversal', testStaticPathTraversal);
|
|
177
|
-
await run('https redirect host poisoning', testHttpsRedirectHostPoisoning);
|
|
178
|
-
|
|
179
|
-
const failed = results.filter(r => !r.ok);
|
|
180
|
-
for (const r of results) {
|
|
181
|
-
// eslint-disable-next-line no-console
|
|
182
|
-
console.log(`${r.ok ? 'PASS' : 'FAIL'} - ${r.name}${r.ok ? '' : `: ${r.error}`}`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (failed.length) {
|
|
186
|
-
process.exitCode = 1;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Only run when executed directly
|
|
191
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
192
|
-
main();
|