← 中文资源

简体中文版本

Permissions Policy

Permissions Policy 让页面声明自身及所嵌入源可以使用哪些浏览器能力(相机、定位、全屏等),通过默认收缩攻击面。

阅读英文原文

概念定义

浏览器暴露的强力功能默认“请求即可用”。Permissions Policy 反转模型:顶层页面按功能声明允许集合,集合之外的源或框架看到的功能如同不存在。被注入的脚本或恶意嵌入能尝试的东西因此被结构性削减。

工作机制

头部按功能列出指令,如 camera=(self https://partner.example) 或 geolocation=()。iframe 必须同时满足父页面策略与自身 allow 属性才能获得委托能力。被拒绝上下文里的特性检测返回“不支持”而不是抛策略错误,探测保持优雅。

使用场景

嵌入第三方组件的新闻与内容站应拒绝自己从不使用的功能;托管用户内容的平台应只放行特定内容所需的能力,例如视频嵌入的全屏。默认拒绝的姿态能把配置错误变成显式失败。

常见误区

该策略只做限制不做授予:底层能力仍需自身的用户授权或 API 调用。功能名随 Web 平台演进,未知令牌会被忽略,清单需要定期复审。iframe 的 allow 属性是独立委托路径,必须与父策略保持同步。

代码示例

export function parseContentDisposition(header) {
  if (typeof header !== "string" || header.trim() === "") {
    throw new TypeError("header must be a non-empty string");
  }
  const segments = splitParameters(header);
  const type = segments.shift().trim().toLowerCase();
  if (type !== "inline" && type !== "attachment") {
    throw new Error(`unsupported disposition type "${type}"`);
  }
  const parameters = new Map();
  for (const segment of segments) {
    const equals = segment.indexOf("=");
    if (equals === -1) continue;
    const name = segment.slice(0, equals).trim().toLowerCase();
    if (name === "") continue;
    parameters.set(name, unquote(segment.slice(equals + 1).trim()));
  }
  return { type, filename: resolveFilename(parameters), parameters };
}

function splitParameters(header) {
  const segments = [];
  let current = "";
  let quoted = false;
  for (const character of header) {
    if (character === '"') quoted = !quoted;
    if (character === ";" && !quoted) {
      segments.push(current);
      current = "";
      continue;
    }
    current += character;
  }
  segments.push(current);
  return segments;
}

function unquote(value) {
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
    return value.slice(1, -1).replace(/\\(.)/g, "$1");
  }
  return value;
}

function resolveFilename(parameters) {
  const extended = parameters.get("filename*");
  if (extended !== undefined) {
    const match = /^([\w-]+)'([\w-]*)'(.+)$/.exec(extended);
    if (match !== null) {
      try {
        return sanitize(decodeURIComponent(match[3]));
      } catch {
        return sanitize(extended);
      }
    }
  }
  const plain = parameters.get("filename");
  return plain === undefined ? null : sanitize(plain);
}

function sanitize(filename) {
  return filename.split(/[\\/]/).pop() || null;
}

console.log(parseContentDisposition('attachment; filename="report.pdf"'));
console.log(parseContentDisposition("attachment; filename*=UTF-8''%E4%B8%AD%E6%96%87.pdf"));
console.log(parseContentDisposition('inline; filename="../../etc/passwd"'));
package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"
)

type Shipment struct {
	ID     string `json:"id"`
	Status string `json:"status"`
}

func queryShipment(ctx context.Context, baseURL, id string) (*Shipment, error) {
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel() // 必须释放计时器资源

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/shipments/"+id, nil)
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Accept", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			return nil, fmt.Errorf("shipment %s timed out after 3s: %w", id, err)
		}
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	var shipment Shipment
	if err := json.NewDecoder(resp.Body).Decode(&shipment); err != nil {
		return nil, fmt.Errorf("decode body: %w", err)
	}
	return &shipment, nil
}

func main() {
	s, err := queryShipment(context.Background(), "https://api.example.com", "sh-1")
	fmt.Println(s, err)
}