Skip to content

Commit 7192199

Browse files
committed
Simplify theme capabilities and reject encoded IP origins
1 parent a337cb9 commit 7192199

3 files changed

Lines changed: 23 additions & 7 deletions

File tree

‎packages/app/src/cli/models/extensions/schemas.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const IframeCapabilitySchema = zod.object({
2121
sources: zod.array(zod.string()).optional(),
2222
})
2323

24-
export const CapabilitiesSchema = zod.object({
24+
const CapabilitiesSchema = zod.object({
2525
// Theme app extensions declare `network_access` as a table that lists the origins they contact (see the theme
2626
// specification). Every other extension declares a boolean, which is all this shared schema accepts. The wider
2727
// type lets the theme specification reuse `BaseConfigType`.

‎packages/app/src/cli/models/extensions/specifications/theme.test.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,20 @@ describe('theme', () => {
5252
expect(parse({}).data?.capabilities?.network_access).toBeUndefined()
5353
})
5454

55-
test('ignores a boolean network_access, as before', () => {
56-
const result = parse({network_access: true})
55+
test.each([true, false])('ignores a boolean network_access of %s, as before', (networkAccess) => {
56+
const result = parse({network_access: networkAccess})
5757

5858
expect(result.state).toBe('ok')
5959
expect(result.data?.capabilities?.network_access).toBeUndefined()
6060
})
6161

62+
test('ignores capabilities that do not apply to theme extensions', () => {
63+
const result = parse({network_access: {allowed_sources: []}, api_access: true, block_progress: true})
64+
65+
expect(result.state).toBe('ok')
66+
expect(result.data?.capabilities).toEqual({network_access: {allowed_sources: []}})
67+
})
68+
6269
test('requires allowed_sources when the network_access table is declared', () => {
6370
const result = parse({network_access: {}})
6471

@@ -99,6 +106,13 @@ describe('theme', () => {
99106
['a wildcard that is not the leading label', 'https://api.*.example.com'],
100107
['a single-label host', 'https://localhost'],
101108
['an IP address', 'https://127.0.0.1'],
109+
['an IP address with a hexadecimal final label', 'https://127.0.0x1'],
110+
['an IP address in hexadecimal notation', 'https://0x7f.0x1'],
111+
['a link-local IP address in hexadecimal notation', 'https://0xa9.0xfe.0xa9.0xfe'],
112+
['a wildcard on an IP address', 'https://*.127.0.0.1'],
113+
['a wildcard on an IP address in hexadecimal notation', 'https://*.0x7f.0x1'],
114+
['a host that cannot be parsed as a URL', 'https://example.0xabc'],
115+
['a wildcard on a host that cannot be parsed as a URL', 'https://*.example.0xabc'],
102116
['an invalid label', 'https://-api.example.com'],
103117
['an empty label', 'https://api..example.com'],
104118
['a trailing dot', 'https://api.example.com./'],

‎packages/app/src/cli/models/extensions/specifications/theme.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import {createExtensionSpecification} from '../specification.js'
2-
import {BaseSchema, CapabilitiesSchema} from '../schemas.js'
2+
import {BaseSchema} from '../schemas.js'
33
import {themeExtensionFiles} from '../../../utilities/extensions/theme.js'
44
import {ExtensionInstance} from '../extension-instance.js'
55
import {fileSize} from '@shopify/cli-kit/node/fs'
66
import {dirname, relativePath} from '@shopify/cli-kit/node/path'
77
import {AbortError} from '@shopify/cli-kit/node/error'
88
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'
99
import {zod} from '@shopify/cli-kit/node/schema'
10+
import {safeParseURL} from '@shopify/cli-kit/common/url'
11+
import {isIP} from 'node:net'
1012

1113
export const MAX_ALLOWED_SOURCES = 20
1214
const MAX_HOST_LENGTH = 253
@@ -34,8 +36,8 @@ export function normalizeAllowedSource(source: string): string | undefined {
3436

3537
const labels = host.split('.')
3638
if (labels.length < 2 || !labels.every((label) => HOST_LABEL_PATTERN.test(label))) return undefined
37-
// A numeric last label means an IP address (or a TLD that doesn't exist).
38-
if (/^\d+$/.test(labels[labels.length - 1]!)) return undefined
39+
const parsedOrigin = safeParseURL(`https://${host}`)
40+
if (!parsedOrigin || isIP(parsedOrigin.hostname) !== 0) return undefined
3941

4042
let portSuffix = ''
4143
if (port !== undefined) {
@@ -71,7 +73,7 @@ const ThemeNetworkAccessSchema = zod.object(
7173
{invalid_type_error: 'network_access must be a table with an allowed_sources list for theme app extensions'},
7274
)
7375

74-
const ThemeCapabilitiesSchema = CapabilitiesSchema.extend({
76+
const ThemeCapabilitiesSchema = zod.object({
7577
// A boolean `network_access` (used by UI extensions) never had an effect on theme app extensions. Keep ignoring it.
7678
network_access: zod.preprocess(
7779
(value) => (typeof value === 'boolean' ? undefined : value),

0 commit comments

Comments
 (0)