Replace CSRF cookie with CrossOriginProtection (#36183)

Removes the CSRF cookie in favor of
[`CrossOriginProtection`](https://pkg.go.dev/net/http#CrossOriginProtection)
which relies purely on HTTP headers.

Fixes: https://github.com/go-gitea/gitea/issues/11188
Fixes: https://github.com/go-gitea/gitea/issues/30333
Helps: https://github.com/go-gitea/gitea/issues/35107

TODOs:

- [x] Fix tests
- [ ] Ideally add tests to validates the protection

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2025-12-25 11:33:34 +01:00
committed by GitHub
parent eddf875992
commit 42d294941c
207 changed files with 178 additions and 1196 deletions

View File

@@ -3,7 +3,7 @@ import {computed, onMounted, onUnmounted, shallowRef, watch} from 'vue';
import {SvgIcon} from '../svg.ts';
import {toggleElem} from '../utils/dom.ts';
const {csrfToken, pageData} = window.config;
const {pageData} = window.config;
const mergeForm = pageData.pullRequestMergeForm;
@@ -95,7 +95,6 @@ function clearMergeMessage() {
<!-- another similar form is in pull.tmpl (manual merge)-->
<form class="ui form form-fetch-action" v-if="showActionForm" :action="mergeForm.baseLink+'/merge'" method="post">
<input type="hidden" name="_csrf" :value="csrfToken">
<input type="hidden" name="head_commit_id" v-model="mergeForm.pullHeadCommitID">
<input type="hidden" name="merge_when_checks_succeed" v-model="autoMergeWhenSucceed">
<input type="hidden" name="force_merge" v-model="forceMerge">
@@ -177,7 +176,6 @@ function clearMergeMessage() {
<!-- the cancel auto merge button -->
<form v-if="mergeForm.hasPendingPullRequestMerge" :action="mergeForm.baseLink+'/cancel_auto_merge'" method="post" class="tw-ml-4">
<input type="hidden" name="_csrf" :value="csrfToken">
<button class="ui button">
{{ mergeForm.textAutoMergeCancelSchedule }}
</button>

View File

@@ -28,7 +28,6 @@ export default defineComponent({
data() {
const shouldShowTabBranches = this.elRoot.getAttribute('data-show-tab-branches') === 'true';
return {
csrfToken: window.config.csrfToken,
allItems: [] as ListItem[],
selectedTab: (shouldShowTabBranches ? 'branches' : 'tags') as SelectedTab,
searchTerm: '',
@@ -272,7 +271,6 @@ export default defineComponent({
{{ textCreateRefFrom.replace('%s', currentRefShortName) }}
</div>
<form ref="createNewRefForm" method="post" :action="createNewRefFormActionUrl">
<input type="hidden" name="_csrf" :value="csrfToken">
<input type="hidden" name="new_branch_name" :value="searchTerm">
<input type="hidden" name="create_tag" :value="String(selectedTab === 'tags')">
<input type="hidden" name="current_path" :value="currentTreePath">

View File

@@ -8,7 +8,7 @@ import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts';
import {isImageFile, isVideoFile} from '../utils.ts';
import type {DropzoneFile, DropzoneOptions} from 'dropzone/index.js';
const {csrfToken, i18n} = window.config;
const {i18n} = window.config;
type CustomDropzoneFile = DropzoneFile & {uuid: string};
@@ -73,7 +73,6 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
let fileUuidDict: FileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
const opts: Record<string, any> = {
url: dropzoneEl.getAttribute('data-upload-url'),
headers: {'X-Csrf-Token': csrfToken},
acceptedFiles: ['*/*', ''].includes(dropzoneEl.getAttribute('data-accepts')!) ? null : dropzoneEl.getAttribute('data-accepts'),
addRemoveLinks: true,
dictDefaultMessage: dropzoneEl.getAttribute('data-default-message'),

View File

@@ -28,7 +28,7 @@ export function initViewedCheckboxListenerFor() {
// To prevent double addition of listeners
form.setAttribute('data-has-viewed-checkbox-listener', String(true));
// The checkbox consists of a div containing the real checkbox with its label and the CSRF token,
// The checkbox consists of a div containing the real checkbox with its label,
// hence the actual checkbox first has to be found
const checkbox = form.querySelector<HTMLInputElement>('input[type=checkbox]')!;
checkbox.addEventListener('input', function() {

View File

@@ -5,7 +5,7 @@ import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {globMatch} from '../utils/glob.ts';
const {appSubUrl, csrfToken} = window.config;
const {appSubUrl} = window.config;
function initRepoSettingsCollaboration() {
// Change collaborator access mode
@@ -56,7 +56,6 @@ function initRepoSettingsSearchTeamBox() {
rawResponse: true,
apiSettings: {
url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`,
headers: {'X-Csrf-Token': csrfToken},
onResponse(response: any) {
const items: Array<Record<string, any>> = [];
for (const item of response.data) {

View File

@@ -1,11 +1,6 @@
import {isObject} from '../utils.ts';
import type {RequestOpts} from '../types.ts';
const {csrfToken} = window.config;
// safe HTTP methods that don't need a csrf token
const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
// fetch wrapper, use below method name functions and the `data` option to pass in data
// which will automatically set an appropriate headers. For json content, only object
// and array types are currently supported.
@@ -20,7 +15,6 @@ export function request(url: string, {method = 'GET', data, headers = {}, ...oth
}
const headersMerged = new Headers({
...(!safeMethods.has(method) && {'x-csrf-token': csrfToken}),
...(contentType && {'content-type': contentType}),
});

View File

@@ -13,7 +13,6 @@ export type Config = {
assetUrlPrefix: string,
runModeIsProd: boolean,
customEmojis: Record<string, string>,
csrfToken: string,
pageData: Record<string, any>,
notificationSettings: Record<string, any>,
enableTimeTracking: boolean,

View File

@@ -7,7 +7,6 @@ window.config = {
assetUrlPrefix: '',
runModeIsProd: true,
customEmojis: {},
csrfToken: 'test-csrf-token-123456',
pageData: {},
notificationSettings: {},
enableTimeTracking: true,