Skip to content

Commit

Permalink
[eas-json] validate EAS Submit inputs better (#2198)
Browse files Browse the repository at this point in the history
* [eas-json] sanitize EAS Submit inputs better

* add link to docs

* update CHANGELOG.md

* validate app specific password

* use Joi schema for validation

* fix typo

* minor improvements

* Update packages/eas-cli/src/submit/ios/__tests__/IosSubmitCommand-test.ts

Co-authored-by: Stanisław Chmiela <[email protected]>

* make uuid validation less strict

* apply suggested changes

* fix tests

---------

Co-authored-by: Stanisław Chmiela <[email protected]>
  • Loading branch information
szdziedzic and sjchmiela authored Jan 26, 2024
1 parent b5f2f52 commit 126c562
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores

- Remove support for classic updates release channel in 50+. ([#2189](https://github.com/expo/eas-cli/pull/2189) by [@wschurman](https://github.com/wschurman))
- Validate EAS Submit inputs better. ([#2198](https://github.com/expo/eas-cli/pull/2198) by [@szdziedzic](https://github.com/szdziedzic))

## [7.0.0](https://github.com/expo/eas-cli/releases/tag/v7.0.0) - 2024-01-19

Expand Down
5 changes: 5 additions & 0 deletions packages/eas-cli/src/submit/ios/IosSubmitCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ export default class IosSubmitCommand {
const envAppSpecificPassword = getenv.string('EXPO_APPLE_APP_SPECIFIC_PASSWORD', '');

if (envAppSpecificPassword) {
if (!/^[a-z]{4}-[a-z]{4}-[a-z]{4}-[a-z]{4}$/.test(envAppSpecificPassword)) {
throw new Error(
'EXPO_APPLE_APP_SPECIFIC_PASSWORD must be in the format XXXX-XXXX-XXXX-XXXX, where X is a lowercase letter.'
);
}
return result({
sourceType: AppSpecificPasswordSourceType.userDefined,
appSpecificPassword: envAppSpecificPassword,
Expand Down
53 changes: 45 additions & 8 deletions packages/eas-cli/src/submit/ios/__tests__/IosSubmitCommand-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,43 @@ describe(IosSubmitCommand, () => {
jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockJester.accounts[0]);
});

it('throws an error if using app specific password in invalid format', async () => {
const projectId = uuidv4();
const graphqlClient = {} as any as ExpoGraphqlClient;
const analytics = instance(mock<Analytics>());
jest
.mocked(getArchiveAsync)
.mockImplementation(jest.requireActual('../../ArchiveSource').getArchiveAsync);

process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'ls -la';

const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
archiveFlags: {
url: 'http://expo.dev/fake.ipa',
},
profile: {
language: 'en-US',
appleId: '[email protected]',
ascAppId: '12345678',
},
nonInteractive: false,
actor: mockJester,
graphqlClient,
analytics,
exp: testProject.appJSON.expo,
projectId,
vcsClient,
});
const command = new IosSubmitCommand(ctx);
await expect(command.runAsync()).rejects.toThrow(
'EXPO_APPLE_APP_SPECIFIC_PASSWORD must be in the format XXXX-XXXX-XXXX-XXXX, where X is a lowercase letter.'
);

delete process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD;
});

describe('non-interactive mode', () => {
it("throws error if didn't provide appleId and ascAppId in the submit profile", async () => {
const projectId = uuidv4();
Expand Down Expand Up @@ -118,7 +155,7 @@ describe(IosSubmitCommand, () => {
.mocked(getArchiveAsync)
.mockImplementation(jest.requireActual('../../ArchiveSource').getArchiveAsync);

process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd';

const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
Expand Down Expand Up @@ -147,7 +184,7 @@ describe(IosSubmitCommand, () => {
archiveSource: { type: SubmissionArchiveSourceType.Url, url: 'http://expo.dev/fake.ipa' },
config: {
appleIdUsername: '[email protected]',
appleAppSpecificPassword: 'supersecret',
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
ascAppIdentifier: '12345678',
},
});
Expand Down Expand Up @@ -182,7 +219,7 @@ describe(IosSubmitCommand, () => {
return ctx;
});

process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd';

const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
Expand All @@ -209,7 +246,7 @@ describe(IosSubmitCommand, () => {
submittedBuildId: selectedBuild.id,
config: {
appleIdUsername: '[email protected]',
appleAppSpecificPassword: 'supersecret',
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
ascAppIdentifier: '87654321',
},
archiveSource: undefined,
Expand Down Expand Up @@ -239,7 +276,7 @@ describe(IosSubmitCommand, () => {
return ctx;
});

process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd';

const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
Expand All @@ -266,7 +303,7 @@ describe(IosSubmitCommand, () => {
submittedBuildId: selectedBuild.id,
config: {
appleIdUsername: '[email protected]',
appleAppSpecificPassword: 'supersecret',
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
ascAppIdentifier: '12345678',
},
archiveSource: undefined,
Expand Down Expand Up @@ -301,7 +338,7 @@ describe(IosSubmitCommand, () => {
return ctx;
});

process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd';

const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
Expand Down Expand Up @@ -329,7 +366,7 @@ describe(IosSubmitCommand, () => {
submittedBuildId: selectedBuild.id,
config: {
appleIdUsername: '[email protected]',
appleAppSpecificPassword: 'supersecret',
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
ascAppIdentifier: '12345678',
},
archiveSource: undefined,
Expand Down
155 changes: 135 additions & 20 deletions packages/eas-json/src/__tests__/submitProfiles-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ test('ios config with all required values', async () => {
ios: {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
appleTeamId: 'AB32CDE81F',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a',
ascApiKeyId: 'AB32CDE81F',
},
},
},
Expand All @@ -121,11 +121,11 @@ test('ios config with all required values', async () => {

expect(iosProfile).toEqual({
appleId: '[email protected]',
appleTeamId: 'QWERTY',
appleTeamId: 'AB32CDE81F',
ascAppId: '1223423523',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a',
ascApiKeyId: 'AB32CDE81F',
language: 'en-US',
});
});
Expand All @@ -137,7 +137,7 @@ test('ios config with ascApiKey fields set to env var', async () => {
ios: {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
appleTeamId: 'AB32CDE81F',
ascApiKeyPath: '$ASC_API_KEY_PATH',
ascApiKeyIssuerId: '$ASC_API_KEY_ISSUER_ID',
ascApiKeyId: '$ASC_API_KEY_ID',
Expand All @@ -148,18 +148,18 @@ test('ios config with ascApiKey fields set to env var', async () => {

try {
process.env.ASC_API_KEY_PATH = './path-ABCD.p8';
process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456';
process.env.ASC_API_KEY_ID = 'ABCD';
process.env.ASC_API_KEY_ISSUER_ID = 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a';
process.env.ASC_API_KEY_ID = 'AB32CDE81F';
const accessor = EasJsonAccessor.fromProjectPath('/project');
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');

expect(iosProfile).toEqual({
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
appleTeamId: 'AB32CDE81F',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a',
ascApiKeyId: 'AB32CDE81F',
language: 'en-US',
});
} finally {
Expand All @@ -176,16 +176,16 @@ test('valid profile extending other profile', async () => {
ios: {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
appleTeamId: 'AB32CDE81F',
},
},
extension: {
extends: 'base',
ios: {
appleTeamId: 'ABCDEF',
appleTeamId: 'AB32CDE81F',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333',
ascApiKeyId: 'AB32CDE81F',
},
},
},
Expand All @@ -202,19 +202,134 @@ test('valid profile extending other profile', async () => {
language: 'en-US',
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'QWERTY',
appleTeamId: 'AB32CDE81F',
});
expect(extendedProfile).toEqual({
language: 'en-US',
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'ABCDEF',
appleTeamId: 'AB32CDE81F',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'abc-123-def-456',
ascApiKeyId: 'ABCD',
ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333',
ascApiKeyId: 'AB32CDE81F',
});
});

test('ios config with with invalid appleId', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {
ios: {
appleId: '| /bin/bash echo "hello"',
ascAppId: '1223423523',
appleTeamId: 'AB32CDE81F',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333',
ascApiKeyId: 'AB32CDE81F',
},
},
},
});

const accessor = EasJsonAccessor.fromProjectPath('/project');
const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
await expect(promise).rejects.toThrow(
'Invalid Apple ID was specified. It should be a valid email address. Example: "[email protected]".'
);
});

test('ios config with with invalid ascAppId', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {
ios: {
appleId: '[email protected]',
ascAppId: 'othervalue',
appleTeamId: 'AB32CDE81F',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333',
ascApiKeyId: 'AB32CDE81F',
},
},
},
});

const accessor = EasJsonAccessor.fromProjectPath('/project');
const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
await expect(promise).rejects.toThrow(
'Invalid Apple App Store Connect App ID ("ascAppId") was specified. It should consist of 10 digits. Example: "1234567891". Learn more: https://expo.fyi/asc-app-id.md.'
);
});

test('ios config with with invalid appleTeamId', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {
ios: {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'ls -la',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333',
ascApiKeyId: 'AB32CDE81F',
},
},
},
});

const accessor = EasJsonAccessor.fromProjectPath('/project');
const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
await expect(promise).rejects.toThrow(
'Invalid Apple Team ID was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CDE81F".'
);
});

test('ios config with with invalid ascApiKeyIssuerId', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {
ios: {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'AB32CDE81F',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'notanuuid',
ascApiKeyId: 'AB32CDE81F',
},
},
},
});

const accessor = EasJsonAccessor.fromProjectPath('/project');
const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
await expect(promise).rejects.toThrow(
'Invalid Apple App Store Connect API Key Issuer ID ("ascApiKeyIssuerId") was specified. It should be a valid UUID. Example: "b4d78f58-48c6-4f2c-96cb-94d8cd76970a". Learn more: https://expo.fyi/creating-asc-api-key.'
);
});

test('ios config with with invalid ascApiKeyId', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
release: {
ios: {
appleId: '[email protected]',
ascAppId: '1223423523',
appleTeamId: 'AB32CDE81F',
ascApiKeyPath: './path-ABCD.p8',
ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a',
ascApiKeyId: 'wrong value',
},
},
},
});

const accessor = EasJsonAccessor.fromProjectPath('/project');
const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
await expect(promise).rejects.toThrow(
`Invalid Apple App Store Connect API Key ID ("ascApiKeyId") was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CDE81F". Learn more: https://expo.fyi/creating-asc-api-key.`
);
});

test('get profile names', async () => {
await fs.writeJson('/project/eas.json', {
submit: {
Expand Down
4 changes: 2 additions & 2 deletions packages/eas-json/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Joi from 'joi';

import { BuildProfileSchema } from './build/schema';
import { SubmitProfileSchema } from './submit/schema';
import { UnresolvedSubmitProfileSchema } from './submit/schema';
import { AppVersionSource } from './types';

export const EasJsonSchema = Joi.object({
Expand All @@ -12,5 +12,5 @@ export const EasJsonSchema = Joi.object({
promptToConfigurePushNotifications: Joi.boolean(),
}),
build: Joi.object().pattern(Joi.string(), BuildProfileSchema),
submit: Joi.object().pattern(Joi.string(), SubmitProfileSchema),
submit: Joi.object().pattern(Joi.string(), UnresolvedSubmitProfileSchema),
});
4 changes: 2 additions & 2 deletions packages/eas-json/src/submit/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import envString from 'env-string';

import { AndroidSubmitProfileSchema, IosSubmitProfileSchema } from './schema';
import { AndroidSubmitProfileSchema, ResolvedIosSubmitProfileSchema } from './schema';
import {
AndroidSubmitProfileFieldsToEvaluate,
IosSubmitProfileFieldsToEvaluate,
Expand Down Expand Up @@ -99,7 +99,7 @@ function mergeProfiles<T extends Platform>(

export function getDefaultProfile<T extends Platform>(platform: T): SubmitProfile<T> {
const Schema =
platform === Platform.ANDROID ? AndroidSubmitProfileSchema : IosSubmitProfileSchema;
platform === Platform.ANDROID ? AndroidSubmitProfileSchema : ResolvedIosSubmitProfileSchema;
return Schema.validate({}, { allowUnknown: false, abortEarly: false, convert: true }).value;
}

Expand Down
Loading

0 comments on commit 126c562

Please sign in to comment.