mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
refactor(mobile): centralize hosted document CSP
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
MobileWebManifestSchema,
|
||||
serializeMobileWebManifestForBuildId
|
||||
} from '../../src/shared/mobile-web/manifest-contract.ts'
|
||||
import { mobileWebDocumentCsp } from '../../src/shared/mobile-web/document-csp.ts'
|
||||
import { MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH } from '../../src/shared/mobile-web/markdown-editor-csp.ts'
|
||||
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
@@ -154,22 +155,7 @@ function replaceReferences(source, replacements) {
|
||||
}
|
||||
|
||||
function mobileWebDocument({ scriptPath, stylePath }) {
|
||||
const csp = [
|
||||
"default-src 'none'",
|
||||
`script-src 'self' ${MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH}`,
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'none'",
|
||||
"media-src 'none'",
|
||||
"object-src 'none'",
|
||||
'frame-src data:',
|
||||
'child-src data:',
|
||||
"worker-src 'none'",
|
||||
"base-uri 'none'",
|
||||
"form-action 'none'",
|
||||
"frame-ancestors 'none'"
|
||||
].join('; ')
|
||||
const csp = mobileWebDocumentCsp(MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH)
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MobileWebManifestSchema,
|
||||
serializeMobileWebManifestForBuildId
|
||||
} from '../../src/shared/mobile-web/manifest-contract.ts'
|
||||
import { mobileWebDocumentCspDirectives } from '../../src/shared/mobile-web/document-csp.ts'
|
||||
import { MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH } from '../../src/shared/mobile-web/markdown-editor-csp.ts'
|
||||
import {
|
||||
MOBILE_WEB_RNW_BUILD_BUDGET,
|
||||
@@ -76,23 +77,9 @@ const html = await readFile(path.join(outputRoot, manifest.entrypoint), 'utf8')
|
||||
if (!/<meta\s+name=["']viewport["'][^>]*\bviewport-fit=cover\b/i.test(html)) {
|
||||
throw new Error('RNW document must expose native safe-area insets')
|
||||
}
|
||||
const requiredCsp = [
|
||||
"default-src 'none'",
|
||||
`script-src 'self' ${MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH}`,
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'none'",
|
||||
"media-src 'none'",
|
||||
"object-src 'none'",
|
||||
'frame-src data:',
|
||||
'child-src data:',
|
||||
"worker-src 'none'",
|
||||
"base-uri 'none'",
|
||||
"form-action 'none'",
|
||||
"frame-ancestors 'none'"
|
||||
]
|
||||
for (const directive of requiredCsp) {
|
||||
for (const directive of mobileWebDocumentCspDirectives(
|
||||
MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH
|
||||
)) {
|
||||
if (!html.includes(directive)) {
|
||||
throw new Error(`RNW CSP is missing: ${directive}`)
|
||||
}
|
||||
|
||||
@@ -1541,8 +1541,10 @@ copy.
|
||||
They also cap each raw manifest document at 256 KiB before JSON parsing.
|
||||
Native activation records also require exact string-typed active/previous
|
||||
hashes; numeric hash-shaped values fail with the same stable error on iOS and
|
||||
Android. Broader generated mutation, path/MIME/CSP, and persisted-cache
|
||||
metadata fuzzing remains open.
|
||||
Android. The packager and verifier now consume one document-CSP contract, and
|
||||
source tests require the iOS and Android response policies to match its exact
|
||||
directive sequence. Broader generated mutation, path/MIME/CSP behavior, and
|
||||
persisted-cache metadata fuzzing remains open.
|
||||
- [ ] Fuzz bridge envelopes, schemas, sizes, IDs, ordering, cancellation, and
|
||||
subscription lifecycle.
|
||||
- [ ] Attempt cross-host, cross-build, cross-workspace, and cross-session races.
|
||||
@@ -2576,4 +2578,5 @@ copy.
|
||||
| 2026-07-28 | Complete | Mirrored 65,537-character native chunk regressions pass the Swift fault executable and the refreshed Android module suite across 76 Gradle tasks. No RNW package content changed. |
|
||||
| 2026-07-28 | Finding | Native manifest parsing enforced asset count and field bounds only after parsing both supplied JSON documents. Swift and Kotlin now reject either raw manifest above 256 KiB before handing it to `JSONSerialization` or `JSONObject`. |
|
||||
| 2026-07-28 | Complete | Mirrored oversized primary/canonical manifest regressions pass the Swift fault executable and the refreshed Android module suite across 76 Gradle tasks. No staging directory is created for the rejected Android inputs. |
|
||||
| 2026-07-28 | Complete | The RNW packager, build verifier, iOS response policy, and Android response policy now share one exact document-CSP contract. Focused packager/native-source tests pass, and a fresh production RNW build retains build `9ed8c7f7…`, 49 assets, 9,280,463 raw bytes, and 2,684,481 gzip bytes. |
|
||||
| 2026-07-28 | Next | Complete the remaining parity inventory and cutover cleanup, then execute the physical-device, topology, security, performance, packaged-release, and App Store gates. |
|
||||
|
||||
@@ -489,6 +489,7 @@ IDs, and unrelated provider state never enter the page result.
|
||||
| -------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Multi-asset manifest v1 | Implemented and focused-test passing | `src/shared/mobile-web/manifest-contract.ts`; TypeScript, Swift, and Kotlin reject quoted/Boolean numeric scalar confusion before staging |
|
||||
| Manifest resource bounds | Implemented, measured, and focused-test passing | 256 KiB/raw manifest before native parse; 48 KiB chunks / 65,536 base64 chars before native decode; 10 MiB/asset, 32 MiB/package, 256 assets |
|
||||
| Document CSP | Implemented and focused-test passing | One shared directive contract drives packaging/verification and exact-sequence checks for both native response policies |
|
||||
| Bridge envelope and capabilities | Implemented and focused-test passing | `src/shared/mobile-web/bridge-contract.ts`; bounded native-chat schemas and remaining operation payload schemas |
|
||||
| Terminal stream | Implemented and focused-test passing | `src/shared/mobile-web/terminal-stream-contract.ts`; broker adapter and real-stream validation remain |
|
||||
| Shell navigation events | Implemented and focused-test passing | Strict opaque routes with monotonic sequence, shell-session/build context, and one-shot restore |
|
||||
|
||||
@@ -2722,8 +2722,10 @@ that encoded ceiling before invoking their native decoders and cap each raw
|
||||
manifest document at 256 KiB before JSON parsing. Native activation metadata
|
||||
likewise requires string-typed active and previous hashes on both platforms;
|
||||
hash-shaped JSON numbers fail with
|
||||
`mobile_web_activation_invalid`. Generated mutation and the remaining
|
||||
path/MIME/CSP/cache corpus are still required.
|
||||
`mobile_web_activation_invalid`. The RNW packager and verifier consume one
|
||||
document-CSP contract, and source tests require both native response policies
|
||||
to match its exact directive sequence. Generated mutation and the remaining
|
||||
path/MIME/CSP behavior/cache corpus are still required.
|
||||
|
||||
## App Store Gate
|
||||
|
||||
|
||||
@@ -210,8 +210,10 @@ cross-scope races, privacy/authorization audit, and independent review.
|
||||
rejects numeric active/previous hashes with the same stable error on both
|
||||
platforms. Both native stores cap each raw manifest at 256 KiB before JSON
|
||||
parsing. Android now requires the exact root document URL and rejects
|
||||
percent-encoded or query-bearing asset requests; a fresh exact-app rerun,
|
||||
generated mutation, and the other listed boundaries remain.
|
||||
percent-encoded or query-bearing asset requests. One document-CSP contract
|
||||
now drives packaging/verification and exact native source parity; a fresh
|
||||
exact-app rerun, generated mutation, and the other listed boundaries
|
||||
remain.
|
||||
- [ ] Attempt cross-host, cross-build, cross-workspace, cross-session, replay,
|
||||
reconnect, process-loss, and host-removal races.
|
||||
- [ ] Verify no credential or privileged host identity reaches URLs, DOM state,
|
||||
|
||||
+1
-1
@@ -30,8 +30,8 @@ private val MOBILE_WEB_CSP = listOf(
|
||||
"default-src 'none'",
|
||||
"script-src 'self' 'sha256-1U6xDmOrcY3IC5LxY6dRlxDPeS9l4iILlzMspyz5qlY='",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"font-src 'self'",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'none'",
|
||||
"media-src 'none'",
|
||||
"object-src 'none'",
|
||||
|
||||
@@ -10,8 +10,8 @@ private let mobileWebCsp = [
|
||||
"script-src 'self' 'sha256-1U6xDmOrcY3IC5LxY6dRlxDPeS9l4iILlzMspyz5qlY='",
|
||||
// Why: React Native Web emits runtime style elements and attributes for the existing mobile UI.
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"font-src 'self'",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'none'",
|
||||
"media-src 'none'",
|
||||
"object-src 'none'",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { MOBILE_WEB_BRIDGE_MAX_MESSAGE_BYTES } from '../../../src/shared/mobile-web/bridge-contract'
|
||||
import { mobileWebDocumentCspDirectives } from '../../../src/shared/mobile-web/document-csp'
|
||||
import { MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH } from '../../../src/shared/mobile-web/markdown-editor-csp'
|
||||
|
||||
const iosSource = readFileSync(
|
||||
@@ -138,6 +139,9 @@ describe('mobile web native bridge transport', () => {
|
||||
})
|
||||
|
||||
it('allows only opaque data frames for the shared rich Markdown editor', () => {
|
||||
const expected = mobileWebDocumentCspDirectives(MOBILE_RICH_MARKDOWN_EDITOR_SCRIPT_CSP_HASH)
|
||||
expect(nativeCspDirectives(iosSource, 'mobileWebCsp')).toEqual(expected)
|
||||
expect(nativeCspDirectives(androidSource, 'MOBILE_WEB_CSP')).toEqual(expected)
|
||||
for (const source of [iosSource, androidSource]) {
|
||||
expect(source).toContain('"frame-src data:"')
|
||||
expect(source).toContain('"child-src data:"')
|
||||
@@ -164,3 +168,16 @@ describe('mobile web native bridge transport', () => {
|
||||
expect(androidSource).toContain('!request.isForMainFrame && url.scheme == "data"')
|
||||
})
|
||||
})
|
||||
|
||||
function nativeCspDirectives(source: string, declaration: string): string[] {
|
||||
const kotlinStart = source.indexOf(`${declaration} = listOf(`)
|
||||
const swiftStart = source.indexOf(`${declaration} = [`)
|
||||
const opening = kotlinStart >= 0 ? kotlinStart : swiftStart
|
||||
const closing = source.indexOf(kotlinStart >= 0 ? ').joinToString' : '].joined', opening)
|
||||
if (opening < 0 || closing < 0) {
|
||||
return []
|
||||
}
|
||||
return [...source.slice(opening, closing).matchAll(/^\s*"([^"]+)",?$/gm)].map(
|
||||
(match) => match[1]!
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export function mobileWebDocumentCspDirectives(markdownEditorScriptHash: string) {
|
||||
return [
|
||||
"default-src 'none'",
|
||||
`script-src 'self' ${markdownEditorScriptHash}`,
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: blob:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'none'",
|
||||
"media-src 'none'",
|
||||
"object-src 'none'",
|
||||
'frame-src data:',
|
||||
'child-src data:',
|
||||
"worker-src 'none'",
|
||||
"base-uri 'none'",
|
||||
"form-action 'none'",
|
||||
"frame-ancestors 'none'"
|
||||
] as const
|
||||
}
|
||||
|
||||
export function mobileWebDocumentCsp(markdownEditorScriptHash: string): string {
|
||||
return mobileWebDocumentCspDirectives(markdownEditorScriptHash).join('; ')
|
||||
}
|
||||
Reference in New Issue
Block a user