Mobile Development 22 min read

HarmonyOS 6: Building Controllable Request Interception for AI Apps Amid Trust Crisis

Triggered by the APIfox supply-chain attack, this article details HarmonyOS 6's three-layer request interception (onLoadIntercept, onInterceptRequest, WebSchemeHandler) with code examples for whitelist enforcement, resource substitution, audit logging, and AI-specific filters against prompt injection and data leakage.

51CTO HarmonyOS Developer Community
51CTO HarmonyOS Developer Community
51CTO HarmonyOS Developer Community
HarmonyOS 6: Building Controllable Request Interception for AI Apps Amid Trust Crisis

AI Assistant's Trust Gap: Overlooked Security Blind Spots

The article opens with the recent APIfox incident where a compromised update hijacked requests and leaked sensitive data. This highlights a broader issue: AI-driven apps now orchestrate dozens of API calls (weather, maps, health data, remote H5), expanding the attack surface far beyond traditional apps. A single compromised link — such as a manipulated map API response redirecting users to a phishing site — can break the entire trust chain.

Risk Analysis Across Request Layers

The author maps five request layers in their AI assistant architecture and associated risks:

AI Service Call — Risk: API Key Leakage → Consequence: Impersonation to call AI services

Third-Party API — Risk: Man-in-the-Middle Attack → Consequence: Route/location data tampered

Web Card Load — Risk: Malicious H5 Injection → Consequence: User redirected to phishing site

Local Resource — Risk: Resource Replacement → Consequence: Images replaced with malicious content

Config Request — Risk: Config Hijacking → Consequence: AI behavior altered (e.g., prompt replaced)

HarmonyOS Request Interception: Three Complementary Defenses

1. onLoadIntercept — Page-Level Whitelist Enforcement

Intercepts before page load; ideal for URL redirection and domain whitelisting. In the AI assistant, it checks whether a clicked link (e.g., third-party://map) matches an allowed scheme list ( arkts://, third-party://, system://, market://, https://developer.huawei.com/, https://www.example.com/) and blocks any non-whitelisted URL, showing a security warning via AppStorage.

// entry/src/main/ets/Interceptors/WhitelistInterceptor.ets
import { Logger } from '@kit.ArkUI';
import { AppStorage } from '@kit.ArkUI';
import type { OnLoadInterceptEvent } from '@kit.ArkWeb';

export class WhitelistInterceptor {
  private readonly ALLOWED_SCHEMES: string[] = [
    'arkts://',
    'third-party://',
    'system://',
    'market://',
    'https://developer.huawei.com/',
    'https://www.example.com/'
  ];
  private readonly BLOCKED_DOMAINS: string[] = [
    'phishing-site.com',
    'malware-download.com'
  ];

  isUrlSafe(url: string): boolean {
    for (const blocked of this.BLOCKED_DOMAINS) {
      if (url.includes(blocked)) {
        Logger.warn(`Blocked malicious URL: ${url}`);
        return false;
      }
    }
    for (const allowed of this.ALLOWED_SCHEMES) {
      if (url.startsWith(allowed)) return true;
    }
    if (url.startsWith('https://')) {
      const domain = this.extractDomain(url);
      if (this.isDomainWhitelisted(domain)) return true;
    }
    Logger.warn(`URL not in whitelist: ${url}`);
    return false;
  }

  onLoadIntercept(event: OnLoadInterceptEvent): boolean {
    const url = event.data.getRequestUrl();
    if (!this.isUrlSafe(url)) {
      this.showSecurityWarning();
      return true; // intercept
    }
    return false; // allow
  }

  private showSecurityWarning(): void {
    AppStorage.setOrCreate('securityWarning', {
      message: '此链接不在安全白名单内,已拦截',
      timestamp: Date.now()
    });
  }

  private extractDomain(url: string): string {
    try {
      const urlObj = new URL(url);
      return urlObj.hostname;
    } catch {
      return '';
    }
  }

  private isDomainWhitelisted(domain: string): boolean {
    return this.ALLOWED_SCHEMES.some(scheme => scheme.includes(domain));
  }
}

2. onInterceptRequest — Resource Substitution & Cache Control

Fires on each resource request (images, CSS, JS). The interceptor maintains a map of remote URLs to local assets (e.g., https://www.example.com/logo.pnglogo.png). For untrusted image domains, it swaps in a local placeholder ( placeholder.png) and adds a Cache-Control: no-cache, no-store, must-revalidate header to prevent cache poisoning.

// entry/src/main/ets/Interceptors/ResourceInterceptor.ets
import { WebResourceResponse } from '@kit.ArkWeb';
import type { OnInterceptRequestEvent } from '@kit.ArkWeb';
import { Logger } from '@kit.ArkUI';

export class ResourceInterceptor {
  private resourceMap: Map<string, string> = new Map([
    ['https://www.example.com/logo.png', 'logo.png'],
    ['https://developer.huawei.com/icon.png', 'huawei_icon.png']
  ]);
  private readonly IMAGE_EXTENSIONS: string[] = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
  private readonly DEFAULT_PLACEHOLDER: string = 'placeholder.png';

  onInterceptRequest(event: OnInterceptRequestEvent): WebResourceResponse | null {
    if (!event || !event.request) return null;
    const url = event.request.getRequestUrl();

    const mappedResource = this.resourceMap.get(url);
    if (mappedResource) return this.createLocalResponse(mappedResource);

    if (this.isImageUrl(url)) {
      const domain = this.extractDomain(url);
      if (!this.isDomainTrusted(domain)) {
        Logger.warn(`Replacing image from untrusted domain: ${url}`);
        return this.createLocalResponse(this.DEFAULT_PLACEHOLDER);
      }
    }
    return null;
  }

  private createLocalResponse(fileName: string): WebResourceResponse {
    const response = new WebResourceResponse();
    response.setResponseData($rawfile(fileName));
    response.setResponseMimeType(this.getMimeType(fileName));
    response.setResponseCode(200);
    response.setReasonMessage('OK');
    response.setResponseIsReady(true);
    response.setResponseHeader([{
      headerKey: 'Cache-Control',
      headerValue: 'no-cache, no-store, must-revalidate'
    }]);
    return response;
  }

  private isImageUrl(url: string): boolean {
    return this.IMAGE_EXTENSIONS.some(ext => url.toLowerCase().endsWith(ext));
  }

  private getMimeType(fileName: string): string {
    if (fileName.endsWith('.png')) return 'image/png';
    if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg')) return 'image/jpeg';
    if (fileName.endsWith('.gif')) return 'image/gif';
    if (fileName.endsWith('.html')) return 'text/html';
    return 'application/octet-stream';
  }

  private extractDomain(url: string): string {
    try {
      const urlObj = new URL(url);
      return urlObj.hostname;
    } catch {
      return '';
    }
  }

  private isDomainTrusted(domain: string): boolean {
    const trustedDomains = ['developer.huawei.com', 'www.example.com'];
    return trustedDomains.includes(domain);
  }
}

3. WebSchemeHandler — Protocol-Level Header Injection & Audit Logging

All network requests pass through a custom WebSchemeHandler. The handler adds security headers ( X-App-Version, X-Request-ID, X-User-Token, X-Device-ID), redacts sensitive query parameters (token, api_key, password) before logging, forwards the request via RCP (Remote Communication Kit), and records an audit entry (URL, method, status, duration, success flag) capped at 1000 entries. Errors return a 500 response.

// entry/src/main/ets/Interceptors/AuditInterceptor.ets
import { webview } from '@kit.ArkWeb';
import { rcp } from '@kit.RemoteCommunicationKit';
import { Logger } from '@kit.ArkUI';

interface AuditLogEntry {
  url: string;
  method: string;
  statusCode?: number;
  error?: string;
  duration: number;
  success: boolean;
  timestamp: number;
}

export class AuditInterceptor {
  private schemeHandler: webview.WebSchemeHandler = new webview.WebSchemeHandler();
  private auditLogs: Array<AuditLogEntry> = [];

  constructor() {
    this.setupHandler();
  }

  private setupHandler(): void {
    this.schemeHandler.onRequest((request, resourceHandler) => {
      this.handleRequest(request, resourceHandler);
    });
  }

  private async handleRequest(
    request: webview.WebSchemeHandlerRequest,
    resourceHandler: webview.WebResourceHandler
  ): Promise<void> {
    const startTime = Date.now();
    const url = request.getRequestUrl();
    const method = request.getRequestMethod();

    const headers = this.collectHeaders(request);
    headers['X-App-Version'] = '1.0.0';
    headers['X-Request-ID'] = this.generateRequestId();
    headers['X-User-Token'] = this.getUserToken();
    headers['X-Device-ID'] = this.getDeviceId();

    const safeUrl = this.redactSensitiveInfo(url);

    try {
      const session = rcp.createSession({ headers });
      const response = await this.forwardRequest(session, url, method, headers);

      this.recordAuditLog({
        url: safeUrl,
        method,
        statusCode: response.statusCode,
        duration: Date.now() - startTime,
        success: true,
        timestamp: startTime
      });
      this.sendResponse(resourceHandler, response);
    } catch (error) {
      this.recordAuditLog({
        url: safeUrl,
        method,
        error: (error as Error).message,
        duration: Date.now() - startTime,
        success: false,
        timestamp: startTime
      });
      this.sendErrorResponse(resourceHandler);
    }
  }

  private async forwardRequest(
    session: rcp.Session,
    url: string,
    method: string,
    headers: Record<string, string>
  ): Promise<rcp.Response> {
    const requestConfig: rcp.RequestConfiguration = {
      method: method as rcp.RequestMethod,
      headers
    };
    if (['POST', 'PUT', 'PATCH'].includes(method)) {
      const body = await this.getRequestBody();
      requestConfig.body = body;
    }
    return session.fetch(url, requestConfig);
  }

  private recordAuditLog(entry: AuditLogEntry): void {
    this.auditLogs.unshift(entry);
    if (this.auditLogs.length > 1000) this.auditLogs.pop();
    this.uploadAuditLogIfNeeded(entry);
    Logger.info(`[AUDIT] ${entry.method} ${entry.url} - ${entry.statusCode || entry.error} (${entry.duration}ms)`);
  }

  private redactSensitiveInfo(url: string): string {
    let redacted = url.replace(/[?&]token=[^&]+/, '&token=REDACTED');
    redacted = redacted.replace(/[?&]api_key=[^&]+/, '&api_key=REDACTED');
    redacted = redacted.replace(/[?&]password=[^&]+/, '&password=REDACTED');
    return redacted;
  }

  private collectHeaders(request: webview.WebSchemeHandlerRequest): Record<string, string> {
    return {};
  }

  private async getRequestBody(): Promise<rcp.Body> {
    return '';
  }

  private generateRequestId(): string {
    return Math.random().toString(36).substring(2, 15);
  }

  private getUserToken(): string {
    return 'user-token-placeholder';
  }

  private getDeviceId(): string {
    return 'device-id-placeholder';
  }

  private sendResponse(resourceHandler: webview.WebResourceHandler, response: rcp.Response): void {
    resourceHandler.setResponseHeader(response.headers);
    resourceHandler.setResponseData(response.body as rcp.Body);
    resourceHandler.setResponseCode(response.statusCode);
    resourceHandler.setReasonMessage('OK');
  }

  private sendErrorResponse(resourceHandler: webview.WebResourceHandler): void {
    resourceHandler.setResponseCode(500);
    resourceHandler.setReasonMessage('Internal Server Error');
  }

  private uploadAuditLogIfNeeded(entry: AuditLogEntry): void {}

  getSchemeHandler(): webview.WebSchemeHandler {
    return this.schemeHandler;
  }
}

Integration into AI Assistant Page

The three interceptors are wired into the Web component in ChatPage.ets: onLoadIntercept for whitelist checks, onInterceptRequest for resource substitution, and onControllerAttached to register the WebSchemeHandler for HTTPS scheme.

// entry/src/main/ets/pages/ChatPage.ets
import { WhitelistInterceptor } from '../Interceptors/WhitelistInterceptor';
import { ResourceInterceptor } from '../Interceptors/ResourceInterceptor';
import { AuditInterceptor } from '../Interceptors/AuditInterceptor';
import { WebSchemeHandler } from '@kit.ArkWeb';

@Entry
@ComponentV2
export struct ChatPage {
  private whitelistInterceptor: WhitelistInterceptor = new WhitelistInterceptor();
  private resourceInterceptor: ResourceInterceptor = new ResourceInterceptor();
  private auditInterceptor: AuditInterceptor = new AuditInterceptor();

  build() {
    Stack() {
      Web({ src: $rawfile('jump_card_embed.html'), controller: this.webController })
        .width(0)
        .height(0)
        .opacity(0)
        .enableNativeEmbedMode(true)
        .onLoadIntercept((event) => {
          const url = event.data.getRequestUrl();
          if (!this.whitelistInterceptor.isUrlSafe(url)) {
            AppStorage.setOrCreate('securityWarning', {
              message: '检测到不安全链接,已拦截',
              url: this.redactSensitiveInfo(url)
            });
            return true;
          }
          return false;
        })
        .onInterceptRequest((event) => {
          const localResponse = this.resourceInterceptor.onInterceptRequest(event);
          if (localResponse) return localResponse;
          return null;
        })
        .onControllerAttached(() => {
          this.webController.registerCustomScheme('https', this.auditInterceptor.getSchemeHandler());
        })
    }
  }

  private redactSensitiveInfo(url: string): string {
    let redacted = url.replace(/[?&]token=[^&]+/, '&token=REDACTED');
    redacted = redacted.replace(/[?&]api_key=[^&]+/, '&api_key=REDACTED');
    redacted = redacted.replace(/[?&]password=[^&]+/, '&password=REDACTED');
    return redacted;
  }
}

AI-Specific Security Extensions

Prompt Injection Filter

A client-side PromptSecurityFilter scans user input for patterns like ignore.*previous.*instruction, system.*prompt,

api[_
]*key

, token, password (case-insensitive) and replaces matches with [REDACTED] while logging a warning.

class PromptSecurityFilter {
  private readonly SENSITIVE_PATTERNS: RegExp[] = [
    /ignore.*previous.*instruction/i,
    /system.*prompt/i,
    /api[_
]*key/i,
    /token/i,
    /password/i
  ];

  filterUserInput(input: string): string {
    let filtered = input;
    for (const pattern of this.SENSITIVE_PATTERNS) {
      if (pattern.test(filtered)) {
        Logger.warn(`Sensitive pattern detected: ${pattern}`);
        filtered = filtered.replace(pattern, '[REDACTED]');
      }
    }
    return filtered;
  }
}

AI Response Safety Check

ResponseSecurityFilter

scans AI output for unsafe URLs (using the same whitelist logic) and JavaScript injection vectors ( <script, javascript:, on\[w\]+\s*=\s*). If any check fails, the response is rejected.

class ResponseSecurityFilter {
  checkForUnsafeUrls(content: string): boolean {
    const urlPattern = /https?:\/\/[^\s]+/g;
    const urls = content.match(urlPattern) || [];
    for (const url of urls) {
      if (!this.isUrlSafe(url)) {
        Logger.warn(`Unsafe URL detected in AI response: ${url}`);
        return false;
      }
    }
    return true;
  }

  checkForJavaScript(content: string): boolean {
    const jsPatterns = [
      /<script/i,
      /javascript:/i,
      /on\w+\s*=/i
    ];
    return jsPatterns.some(pattern => pattern.test(content));
  }

  private isUrlSafe(url: string): boolean {
    return true; // placeholder for actual whitelist logic
  }
}

Sensitive Data Masking for Logs

DataMasking.maskSensitiveData

applies regex replacements: phone numbers ( 1[3-9]\d\d{4}\d{4} → first 3 and last 4 digits), emails (first 2 and last 2 chars before @), ID cards (first 4 and last 4 digits), and coordinates (truncated to two decimal places).

class DataMasking {
  static maskSensitiveData(text: string): string {
    let masked = text;
    masked = masked.replace(/(1[3-9]\d)\d{4}(\d{4})/, '$1****$2');
    masked = masked.replace(/([^@]{2})[^@]*([^@]{2})@/, '$1***$2@');
    masked = masked.replace(/(\d{4})\d{10}(\d{4})/, '$1**********$2');
    masked = masked.replace(/(\d{2,3}\.\d{2})\d+/, '$1');
    return masked;
  }
}

Conclusion: Security Is the Lifeline of AI Apps

The APIfox incident forces a shift from "adding features" to "subtracting risks". The three-layer interception (page, resource, protocol) plus AI-specific filters form a controllable, auditable defense that keeps security authority inside the app. Trust in AI-era applications is built on such proactive, layered protection.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

prompt injectionAI securityaudit loggingrequest interceptiondata maskingArkWebHarmonyOS 6onInterceptRequestonLoadInterceptWebSchemeHandler
51CTO HarmonyOS Developer Community
Written by

51CTO HarmonyOS Developer Community

The HarmonyOS Developer Community is a learning-oriented community for developers to learn, communicate, ask questions, and share.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.