@appliance.sh/api-server 1.22.0 → 1.23.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appliance.sh/api-server",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "description": "",
5
5
  "author": "Eliot Lim",
6
6
  "repository": "https://github.com/appliance-sh/appliance.sh",
@@ -19,8 +19,8 @@
19
19
  "test:e2e": "vitest run --config vitest.e2e.config.ts"
20
20
  },
21
21
  "dependencies": {
22
- "@appliance.sh/infra": "1.22.0",
23
- "@appliance.sh/sdk": "1.22.0",
22
+ "@appliance.sh/infra": "1.23.0",
23
+ "@appliance.sh/sdk": "1.23.0",
24
24
  "@aws-sdk/client-ecr": "^3.1005.0",
25
25
  "@aws-sdk/client-s3": "^3.750.0",
26
26
  "express": "^5.2.1"
@@ -1,11 +1,12 @@
1
1
  import { Router } from 'express';
2
- import { timingSafeEqual } from 'crypto';
2
+ import { createHash, timingSafeEqual } from 'crypto';
3
3
  import { apiKeyInput } from '@appliance.sh/sdk';
4
4
  import { apiKeyService } from '../../services/api-key.service';
5
5
 
6
6
  function constantTimeEqual(a: string, b: string): boolean {
7
- if (a.length !== b.length) return false;
8
- return timingSafeEqual(Buffer.from(a), Buffer.from(b));
7
+ const ha = createHash('sha256').update(a).digest();
8
+ const hb = createHash('sha256').update(b).digest();
9
+ return timingSafeEqual(ha, hb);
9
10
  }
10
11
 
11
12
  export const bootstrapRoutes = Router();
@@ -2,7 +2,7 @@ import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
2
2
  import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr';
3
3
  import { applianceBaseConfig, applianceInput } from '@appliance.sh/sdk';
4
4
  import type { ApplianceContainer, ApplianceFrameworkApp } from '@appliance.sh/sdk';
5
- import { execSync } from 'child_process';
5
+ import { execFileSync } from 'child_process';
6
6
  import * as fs from 'node:fs';
7
7
  import * as path from 'node:path';
8
8
  import * as os from 'node:os';
@@ -60,7 +60,8 @@ export class BuildService {
60
60
 
61
61
  try {
62
62
  // List zip contents and validate paths before extracting
63
- const entries = execSync(`zipinfo -1 "${zipPath}"`, { encoding: 'utf-8' }).trim().split('\n');
63
+ // Validate zip contents before extracting
64
+ const entries = execFileSync('zipinfo', ['-1', zipPath], { encoding: 'utf-8' }).trim().split('\n');
64
65
  for (const entryPath of entries) {
65
66
  const resolved = path.resolve(tmpDir, entryPath);
66
67
  if (!resolved.startsWith(tmpDir + path.sep) && resolved !== tmpDir) {
@@ -68,7 +69,15 @@ export class BuildService {
68
69
  }
69
70
  }
70
71
 
71
- execSync(`unzip -o -q "${zipPath}" -d "${tmpDir}"`, { stdio: 'pipe' });
72
+ execFileSync('unzip', ['-o', '-q', zipPath, '-d', tmpDir], { stdio: 'pipe' });
73
+
74
+ // Reject symlinks after extraction
75
+ for (const entryPath of entries) {
76
+ const fullPath = path.join(tmpDir, entryPath);
77
+ if (fs.existsSync(fullPath) && fs.lstatSync(fullPath).isSymbolicLink()) {
78
+ throw new Error(`Zip contains symlink: ${entryPath}`);
79
+ }
80
+ }
72
81
 
73
82
  // Read the manifest
74
83
  const manifestPath = path.join(tmpDir, 'appliance.json');
@@ -123,10 +132,11 @@ export class BuildService {
123
132
  /**
124
133
  * Container builds are fully pre-processed by the CLI (Lambda Web Adapter
125
134
  * already injected). The server just loads the image and pushes it to ECR.
135
+ * All subprocess calls use execFileSync (array args) to prevent shell injection.
126
136
  */
127
137
  private async resolveContainer(
128
138
  tmpDir: string,
129
- manifest: ApplianceContainer,
139
+ _manifest: ApplianceContainer,
130
140
  tag: string,
131
141
  config: ReturnType<typeof getBaseConfig>
132
142
  ): Promise<ResolvedBuild> {
@@ -134,10 +144,13 @@ export class BuildService {
134
144
  if (!ecrRepositoryUrl) throw new Error('ECR repository not configured');
135
145
 
136
146
  const imageTarPath = path.join(tmpDir, 'image.tar');
137
- if (!fs.existsSync(imageTarPath)) throw new Error('Build missing image.tar');
147
+ if (!fs.existsSync(imageTarPath)) {
148
+ const extracted = fs.readdirSync(tmpDir);
149
+ throw new Error(`Build missing image.tar. Extracted contents: ${extracted.join(', ')}`);
150
+ }
138
151
 
139
152
  // Load the pre-built Lambda-ready image
140
- const loadOutput = execSync(`docker load -i "${imageTarPath}"`, { encoding: 'utf-8' });
153
+ const loadOutput = execFileSync('docker', ['load', '-i', imageTarPath], { encoding: 'utf-8' });
141
154
  const loadedMatch = loadOutput.match(/Loaded image(?: ID)?:\s*(.+)/);
142
155
  if (!loadedMatch) throw new Error(`Failed to parse docker load output: ${loadOutput}`);
143
156
  const loadedImage = loadedMatch[1].trim();
@@ -152,20 +165,20 @@ export class BuildService {
152
165
 
153
166
  const decoded = Buffer.from(authData.authorizationToken, 'base64').toString();
154
167
  const [username, password] = decoded.split(':');
155
- execSync(`docker login --username ${username} --password-stdin ${authData.proxyEndpoint}`, {
168
+ execFileSync('docker', ['login', '--username', username, '--password-stdin', authData.proxyEndpoint], {
156
169
  input: password,
157
170
  stdio: ['pipe', 'pipe', 'pipe'],
158
171
  });
159
172
 
160
173
  // Tag and push
161
174
  const remoteTag = `${ecrRepositoryUrl}:${tag}`;
162
- execSync(`docker tag "${loadedImage}" "${remoteTag}"`, { stdio: 'pipe' });
163
- execSync(`docker push "${remoteTag}"`, { stdio: 'pipe' });
175
+ execFileSync('docker', ['tag', loadedImage, remoteTag], { stdio: 'pipe' });
176
+ execFileSync('docker', ['push', remoteTag], { stdio: 'pipe' });
164
177
 
165
178
  // Get digest
166
179
  let imageUri: string;
167
180
  try {
168
- imageUri = execSync(`docker inspect --format='{{index .RepoDigests 0}}' "${remoteTag}"`, {
181
+ imageUri = execFileSync('docker', ['inspect', '--format={{index .RepoDigests 0}}', remoteTag], {
169
182
  encoding: 'utf-8',
170
183
  }).trim();
171
184
  } catch {