3

Whenever I try to send a POST request from background.js of my Chrome extension I get 403 Forbidden error. If I execute the same code outside my Chrome extension it works normally. My API doesn't require any authentication.

Request code:

let formData = new FormData();
formData.append('message', "This is a test message.");

fetch('https://myapi.com/add', {
    body: formData,
    method: "post"
}).then(r => console.log(r));

Request response:

403 Forbidden response from background.js

I also checked my Apache 2 access.log and everything seems to look normal:

x.x.x.x - - [13/Sep/2020:17:02:30 +0000] "POST add HTTP/1.1" 403 581 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36"

Are there any Chrome extension policies that could block my request?
Do I need to add any special permissions to my manifest.json?
Do I need to make any changes to my API (take this as a reserve, because it works normally outside Chrome extension)?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
duplxey
  • 260
  • 3
  • 11
  • 1
    Does your API require authentication? If you logged into your app in your browser, the auth cookie/token should be sent with all requests, which is why it works. Requests from a chrome extension run in a "sandbox" and don't have access to the same cookies/tokens as the selected browser tab. – Ryan Wheale Sep 13 '20 at 17:48
  • My API doesn't require any authentication. I forgot to point this out, thanks. – duplxey Sep 13 '20 at 17:49
  • https://stackoverflow.com/questions/25107774/how-do-i-send-an-http-get-request-from-a-chrome-extension – Ryan Wheale Sep 13 '20 at 17:54
  • I already had "" permission added. All my GET requests work well. – duplxey Sep 13 '20 at 18:07
  • 1
    In my case it was done due to an extension that I had on Chrome, try disabling all extensions and then try again. This may help someone else. I was stuck 3 days on this one tried everything and in the end the issue was caused by extension... – nikola3103 Feb 15 '22 at 10:07
  • Had the same issue as @nikola3103. I used ModHeader Chrome extension where I set x-api-key to some value. After some time I forgot I ever had that extension and this caused 403 errors from time to time until I realized it was the culprit. – Dmitry Mar 06 '23 at 21:14

3 Answers3

1

Edit: Read your server logs. The server is probably telling you exactly what the error is. Make sure debugging is enabled.

403 errors are not really emitted from a server without good reason - and that reason is usually specific to your application. A 403 is generally a permission issue: we know who you are, but you're not authorized to do what you're trying to do.

There might be a header present (or missing) which is preventing the request from making any changes (in your case, a POST request). For example, your server might be setting cookies to prevent CSRF attacks. Many times the server will not validate tokens on GET requests, which may explain why GET requests work in your case but not POST requests.

If you can search your server code, I'm willing to be there is something like this pseudocode:

if (requestIsMissingCSRFToken()) {
  throw new Error(STATUS.FORBIDDEN)
}

Edit: Some relevant links

Ryan Wheale
  • 26,022
  • 8
  • 76
  • 96
  • My API is written in Django using Django REST framework. I've disabled CSRF protection on all my API views using 'csrf_exempt' and installed 'django-cors-headers' and I still have the same problem. I also tested the API with Postman and apitester.com and it works well. Chrome extension seems to be sending weird requests. I also added all the required permissions as stated in this guide: https://developer.chrome.com/extensions/xhr. – duplxey Sep 14 '20 at 14:10
  • Hmm - I'm not sure then. At this point you're going to need to print the entire request for both successful and failing requests and compare everything until you find a difference. My money says there's some header or cookie present or missing. Django is great because it does a lot for you. It's also a pain because it does a lot for you. Maybe Django is configured to block requests from Chrome extensions by default. I dunno. Good luck. – Ryan Wheale Sep 14 '20 at 15:05
  • The CSRF thing I posted was a just a guess. The more I read the more suspicious I am: https://groups.google.com/g/django-users/c/Z7rKIzyu7VM?pli=1 – Ryan Wheale Sep 14 '20 at 15:13
  • I just checked '403 Forbidden' details and it says this "{detail: "CSRF Failed: Referer checking failed - no Referer."}". This means it almost surely is CSRF. I thought I disabled it with Django @csrf_exempt decorator. – duplxey Sep 14 '20 at 15:26
0

The problem was in Django REST framework. Apparently it has a few authentication classes enabled by default. Disabling/setting them to an empty array in settings.py will get rid of the annoying '403 Forbidden' error.

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
    ]
}

EDIT: I don't know how safe this is, but at least it works.

duplxey
  • 260
  • 3
  • 11
0

I just had the same problem. Thanks so much for posting this. I also work on a Chrome extension with Django backend.

For me deleting 'rest_framework.authentication.SessionAuthentication' from the 'DEFAULT_AUTHENTICATION_CLASSES' did it, but I kept the 'oauth2_provider.contrib.rest_framework.OAuth2Authentication'.

According to this medium article "rest_framework.authenication.SessionAuthentication' is only needed if you want to keep browsable API. I have CSRF and CORS enabled. So I think my setup is very secure.

Below is my settings.py file.

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '*xbmoy2yt4%l=od-dm*w$dxpl+rb(n#rmv0n&0x$a@+io!j+++'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'audio.apps.AudioConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'accounts.apps.AccountsConfig',
    'rest_framework',
    'oauth2_provider',
    #'corsheaders',
]

MIDDLEWARE = [
    #'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'oauth2_provider.middleware.OAuth2TokenMiddleware'
]

ROOT_URLCONF = 'zeno.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'zeno.wsgi.application'
CORS_ORIGIN_ALLOW_ALL = True


# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

CORS_ORIGIN_ALLOW_ALL = False


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = '/static/'
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'

PROJECT_ROOT = os.path.normpath(os.path.dirname(__file__))
STATICFILES_DIRS = (
    os.path.join(PROJECT_ROOT, '..', 'static'),
)

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
        #'rest_framework.authentication.SessionAuthentication', # To keep the Browsable API
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    # 'DEFAULT_PAGINATION_CLASS': (
    #     'rest_framework.pagination.PageNumberPagination',
    # ),
    # 'DEFAULT_PERMISSION_CLASSES': (
    #     'rest_framework.permissions.IsAuthenticated',
    # ),
    #'PAGE_SIZE': 10
}


AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend', # To keep the Browsable API
    'oauth2_provider.backends.OAuth2Backend',
)

#STATIC_ROOT = os.path.join(BASE_DIR, "static/")
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'test.com']

AUTH_USER_MODEL = 'accounts.CustomUser'

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
#EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
#EMAIL_FILE_PATH = os.path.join(BASE_DIR, "sent_emails")
bert
  • 21
  • 1
  • 3