content-grade 1.0.7 → 1.0.9

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.
@@ -90,6 +90,17 @@ const MG = '\x1b[35m';
90
90
  const BL = '\x1b[34m';
91
91
  const WH = '\x1b[97m';
92
92
 
93
+ // ── Input sanitization ────────────────────────────────────────────────────────
94
+
95
+ // Reject null bytes (path traversal vector) and trim surrounding whitespace.
96
+ // Returns sanitized string, or null if the input is invalid.
97
+ function sanitizeFilePath(p) {
98
+ if (typeof p !== 'string') return null;
99
+ if (p.includes('\0')) return null;
100
+ const trimmed = p.trim();
101
+ return trimmed || null;
102
+ }
103
+
93
104
  // ── Helpers ───────────────────────────────────────────────────────────────────
94
105
 
95
106
  function ok(msg) { console.log(` ${GN}✓${R} ${msg}`); }
@@ -257,6 +268,15 @@ async function cmdAnalyze(filePath) {
257
268
  process.exit(2);
258
269
  }
259
270
 
271
+ const safeFilePath = sanitizeFilePath(filePath);
272
+ if (!safeFilePath) {
273
+ blank();
274
+ fail(`Invalid file path.`);
275
+ blank();
276
+ process.exit(2);
277
+ }
278
+ filePath = safeFilePath;
279
+
260
280
  const absPath = resolve(process.cwd(), filePath);
261
281
  if (!existsSync(absPath)) {
262
282
  blank();
@@ -856,6 +876,15 @@ async function cmdBatch(dirPath) {
856
876
  process.exit(2);
857
877
  }
858
878
 
879
+ const safeDirPath = sanitizeFilePath(dirPath);
880
+ if (!safeDirPath) {
881
+ blank();
882
+ fail(`Invalid directory path.`);
883
+ blank();
884
+ process.exit(2);
885
+ }
886
+ dirPath = safeDirPath;
887
+
859
888
  const absDir = resolve(process.cwd(), dirPath);
860
889
  if (!existsSync(absDir)) {
861
890
  blank();
@@ -1439,9 +1468,15 @@ function fetchUrl(url) {
1439
1468
  return new Promise((resolve, reject) => {
1440
1469
  const get = url.startsWith('https') ? httpsGet : httpGet;
1441
1470
  const req = get(url, { headers: { 'User-Agent': 'ContentGrade/1.0 (+https://github.com/content-grade/Content-Grade)' }, timeout: 15000 }, (res) => {
1442
- // Follow one redirect
1471
+ // Follow one redirect — validate location is http/https before following
1443
1472
  if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
1444
- fetchUrl(res.headers.location).then(resolve).catch(reject);
1473
+ const loc = res.headers.location;
1474
+ if (!/^https?:\/\//i.test(loc)) {
1475
+ reject(new Error(`Redirect to non-HTTP location rejected: ${loc.slice(0, 80)}`));
1476
+ res.resume();
1477
+ return;
1478
+ }
1479
+ fetchUrl(loc).then(resolve).catch(reject);
1445
1480
  res.resume();
1446
1481
  return;
1447
1482
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "content-grade",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "AI-powered content analysis CLI. Score any blog post, landing page, or ad copy in under 30 seconds — runs on Claude CLI, no API key needed.",
5
5
  "type": "module",
6
6
  "bin": {